What is different between using DynamicStructuredTool vs tool()?

I would like to standardize the approach for creating and utilizing tools within our project. Currently, some tools are implemented using DynamicStructuredTool directly, while others employ the tool() function to manually return Command objects. Could you please advise which approach would be more suitable for long-term project maintainability and scalability?

hi @carefreelife98

given those files index.ts, types.ts, we could state this:

TL;DR: In LangChain JS, tool() is a higher‑level factory that builds DynamicStructuredTool/DynamicTool for you and wires in newer runtime features (state, context, interrupts, cancellation, etc.). For a long‑lived codebase, standardize on tool() for almost everything and only reach for DynamicStructuredTool when you explicitly need low‑level control or custom subclasses.

What is actually different?

1. DynamicStructuredTool is the low‑level class.

  • You instantiate it directly:
    • new DynamicStructuredTool({ name, description, schema, func, ... }).
    • func(input, runManager?, config?) gets called after the schema has been applied.
  • It’s just a StructuredToolInterface implementation: schema validation + _call wrapper, with no extra opinions.
  • Source: see DynamicStructuredTool in index.ts (around lines 450–511).

2. tool() is a convenience factory with extra plumbing.

  • tool() returns either a DynamicTool (string input) or DynamicStructuredTool (object/JSON schema input) based on the schema you pass.
  • It accepts functions of the form:
    • async (input, runtime: ToolRuntime) => ... or
    • async (input, config: ToolRunnableConfig) => ....
  • Internally (index.ts lines ~744–845 and types.ts lines ~448–548):
    • Wraps your function in the LangChain/LangGraph execution context via AsyncLocalStorageProviderSingleton.
    • Handles AbortSignal cancellation for tools.
    • Injects ToolRuntime (state, context, store, writer, toolCallId, config) when used with LangGraph.
    • Ensures callbacks / metadata / config are propagated consistently.
  • This is the API that current LangChain + LangGraph docs push for defining tools, e.g. the custom tools and interrupts examples (Interrupts - Docs by LangChain).

3. For the LLM, both look the same.

  • Both approaches ultimately expose the same things to the model: name, description, and an argument schema.
  • The difference is ergonomics and integration with the broader runtime, not what the model sees.

About returning Command objects

Returning Command is orthogonal to DynamicStructuredTool vs tool().

  • A tool’s return type in JS is largely unconstrained (ToolOutputType), so you can return a Command from either a manually created DynamicStructuredTool or a tool()-created tool.
  • LangGraph’s runtime special‑cases Command in graph nodes; it doesn’t care whether the underlying tool was made via new DynamicStructuredTool(...) or tool(...).
  • Where tool() does help is giving you the ToolRuntime (state, context, store, writer), which is extremely useful in non-trivial graphs that use Command to orchestrate flow.

Recommendation for maintainability & scalability

I would standardize on tool() for project code.

  • Pros:
    • Less boilerplate; consistent pattern: const myTool = tool(async (input, runtime) => { ... }, { name, description, schema }).
    • Automatically gains new LangChain/LangGraph runtime features (state injection, context, better cancellation, streaming via runtime.writer, etc.) without you changing every tool.
    • Easier onboarding: new engineers learn one idiomatic pattern that matches the official docs.
  • Use DynamicStructuredTool directly only when:
    • You need a custom subclass or to override parts of StructuredTool/invoke.
    • You are building a lower‑level library that wants fine‑grained control over callback plumbing or config types.
    • You are porting/mirroring legacy code that already works at the DynamicStructuredTool level and you have a strong reason not to touch it.

In a typical app that uses LangChain + LangGraph and returns Command objects from tools, the most future‑proof, maintainable approach is: define all new tools with tool() and gradually migrate existing DynamicStructuredTool usages to that helper unless you have a concrete need for the low‑level API.

1 Like

Hi @pawel-twardziak ,
thank you so much for taking the time to write such a detailed and thoughtful explanation. This was incredibly helpful — it clarified not just the what, but the why behind tool() vs DynamicStructuredTool, especially around runtime integration and long-term maintainability.

Your breakdown of the internal plumbing (AsyncLocalStorage, cancellation, ToolRuntime injection, etc.) really helped me connect the docs with the actual implementation, and the recommendation section gives a very clear direction for how to structure a real production codebase. It saved me a lot of trial-and-error.

I really appreciate the depth and clarity here. Thanks again — this was a big help! :slight_smile:

1 Like