Dynamiq
WorkflowsAgents

Agent Tools

Attach tools to the Agent node, write descriptions the model can act on, and pass runtime tool parameters with tool_params.

Tools are what turn an Agent node from a chatbot into a worker. Every tool is a regular workflow node attached to the agent; on each loop iteration the agent's LLM decides whether to call one, which one, and with what input. This page covers attaching tools, how the model chooses between them, the tool catalog, and runtime parameter injection with tool_params.

Attach tools to an agent

Open the Tools section

Select the Agent node and find Tools in the configuration panel. Each attached tool shows a selector, a gear icon to open the tool's own configuration, and a trash icon to detach it.

Tools section of the Agent configuration panel with attached tools, gear and trash icons, and the Add tool link

Add a tool

Click Add tool and pick from the tool catalog — the selector lists every tool node plus other agents (so an agent can be attached as a sub-agent). To attach a Knowledge Base in one click, use Add knowledge, which adds a Knowledge Base Retriever tool.

Configure the tool

Click the gear icon to set the tool's connection and parameters — for example which Tavily connection a web search uses, or the connection string for a SQL Executor. Tools attached to an agent render as child nodes on the canvas, hanging off the Agent node's tool handle.

Agent node on the canvas with attached tool nodes connected below it

Names and descriptions drive tool selection

The agent's system prompt contains one line per tool, built from the tool's name and description:

- tavily-search: Searches the web for current information...
- sql-executor: Executes SQL queries against the configured database...

The LLM has nothing else to go on — it picks a tool by matching the task against these lines, then fills in the input schema. That makes naming the highest-leverage tool setting:

  • Make names say what the tool does. crm-contact-lookup beats tool-2. Names are sanitized before they reach the model: spaces become hyphens and any character outside letters, digits, _, and - is stripped — the model must reference the sanitized name exactly, so keep names simple.
  • Write descriptions for the model, not for teammates. State what the tool does, when to use it, and when not to: "Searches internal HR policies. Use for questions about leave, benefits, or onboarding — not for general web questions."
  • Disambiguate overlapping tools. If an agent has two search tools, say in each description which domains or query types it should own. Vague, overlapping descriptions are the most common cause of an agent calling the wrong tool.

If the model names a tool that does not exist, the agent returns an "Unknown tool" observation listing the constraint, and the model corrects itself on the next loop — but every such round-trip burns one iteration of the Max loop budget.

Tool catalog

Categories of tools you can attach, with the built-in node names the agent sees:

CategoryTools
Web searchTavily (tavily-search), Exa (exa-search), ScaleSerp (scale-serp-search), Firecrawl Search (firecrawl-search), Jina web search
Web scrapingFirecrawl (firecrawl-scrape), ZenRows (zenrows-scrape), Jina (scraping) (jina-scrape)
Code executionE2B code interpreter (e2b-code-interpreter-tool) — Python, shell, and file operations in a remote sandbox; Local Python Code Sandbox (python-code-executor); Custom Python Tool (python-tool) — your own Python function as a tool, see Custom Python tools
DatabasesSQL Executor (sql-executor), Cypher Graph Query for graph databases
HTTPHTTP API Call (api-call) — call any REST endpoint with configurable method, headers, and parameters
ActionsAction (dynamiq.nodes.tools.Pipedream) — run one operation in an external app (send a Gmail message, post to Slack, update a CRM record) from a connector catalog of 1,000+ apps; see Action tools
MCPMCP Server — attach a Model Context Protocol server; each MCP tool it exposes is expanded into an individual agent tool. See MCP Servers for connection setup and tool filtering
KnowledgeKnowledge Base Retriever (via Add knowledge) and Vector Store Retriever for semantic search over your data — see Connect a Knowledge Base to Agents
Human in the loopHuman Feedback — pause and ask a person for approval, confirmation, or missing information; see Human in the Loop
Sub-agentsAny other Agent attached as a tool — see below
Browser automationStagehand (stagehand-browser) — see Browser automation with Stagehand; E2B Desktop Tool
UtilitiesExtended Thinking (thinking-tool) for scratch-pad reasoning, LLM summarizer (llm-summarizer) for cleaning up long text

Skills are a related but separate mechanism — reusable instruction packages enabled in the Skills section of the agent panel rather than attached as individual tools. See Skills.

Sub-agents

Attaching another Agent as a tool creates a sub-agent: the parent delegates a task by calling the child with a brief (a short summary of what it is delegating), an input (the actual task), and optionally files to hand over. Sub-agents are how you keep each agent's role and tool list small while still solving broad tasks.

Two parent-side settings interact with sub-agents:

  • Allow delegation — lets the parent return a sub-agent's answer directly as the final answer (the call sets delegate_final), instead of re-summarizing it. The sub-agent's Description field is what the parent's LLM reads when deciding to delegate, so write it like a tool description.
  • Call limits — a sub-agent tool can carry a maximum number of invocations per run (max_calls in the SDK); once exhausted, further delegation attempts are rejected with an observation telling the parent to use other tools or finish.

When the parent runs with memory identifiers, sub-agents automatically receive scoped ids (user_id and session_id suffixed with the sub-agent name) so their memory never collides with the parent's — see Agent Memory. For multi-agent design patterns and delegation flows, see Subagents & Delegation.

Action tools

An Action tool runs one operation in an external app — send a Gmail message, post a Slack message, update a CRM record — picked from a connector catalog of 1,000+ apps, with no custom HTTP integration. Each Action tool wraps exactly one action of one app; attach several Action tools when the agent needs several operations. The node reference is at Action.

Pick the app and action

Click Add tool and choose Action — the entry marked with a 1000+ tools badge. In the Select App Action sheet, search for an app, expand it to list its actions, and click one to attach it. To hand the agent an app's whole surface at once, click Add all actions on the app row — each action becomes its own tool.

Select App Action sheet with the app search field and an expanded app listing its individual actions

screenshot: workflows-action-select-app-modal

Configure the action's fields

The tool's configuration panel shows the chosen app/action pair under Select action, the action's description, and a form generated from that action's own fields. Anything you fill here is fixed at build time; the remaining fields stay in the tool's input schema for the model to fill at call time. Pre-filled values become optional for the model, and the generated tool description lists them as parameters it may override. Some fields load their options from the linked account (a Slack channel list, a CRM pipeline), so link the account first.

The tool's name and description are generated from the action; like any tool, the description is what the agent's LLM reads when choosing it — edit it if the agent has overlapping Action tools.

The app's account field has two tabs:

  • Accounts — select an account you already linked, or pick Connect new … account to authorize one now. Every run of the workflow then acts through this single account, for all users. Right for shared, organization-owned accounts (a team Slack bot, a support inbox).
  • Requirements — click + New requirement to flag the account as something each end user of the deployed App must connect themselves before the App runs for them. Right when the action must act as the user — reading their inbox, posting as them. Save the workflow first; the Requirements tab is disabled on an unsaved workflow.

Action tool account field with the Accounts / Requirements tabs and the Connect new account option

screenshot: workflows-action-account-tabs

For how per-user requirements are discovered and fulfilled at run time, see End-User Connection Requirements.

At run time the tool returns the action's response as content (plus files when the action produces file artifacts). Failures with status 400, 401, 402, or 422 — bad input or a missing account — come back as recoverable observations the agent can react to; other failures raise a non-recoverable error.

Add an HTTP endpoint as a tool

Any REST endpoint becomes an agent tool through the HTTP API Call node — no custom code needed. Attach it with Add tool, pick HTTP API Call, and open the gear icon:

  1. Connection — an HTTP Connection stores the base url, default method, and any standing headers, params, and data (the right place for API keys, kept out of the workflow definition).
  2. MethodGET, POST, PUT, DELETE, or PATCH.
  3. Request tabs — HEADERS and PARAMS are key-value editors; DATA is a JSON editor for the request body; SETTINGS holds Payload type (raw form data or json body), Response type, Timeout, and the accepted success codes.
  4. Description — when the node is attached to an agent, write what the endpoint does and when to call it, like any tool description.

Configuration reference (SDK field names in parentheses):

urlstring
Endpoint URL. Resolution order: value supplied by the model at call time, then the node URL, then the Connection URL.
methodenum
GET, POST, PUT, DELETE, or PATCH. Same resolution order as url.
headers / params / dataobject
Merged from three layers — Connection values, node values, then per-call input — with later layers overriding earlier ones on key conflicts.
payload_typeenum
"raw" sends data as form data; "json" sends it as a JSON body. Default raw.
response_typeenum
"text", "raw", or "json". Default raw — but if you leave it unset and the response has a JSON content type, it is parsed as JSON automatically.
timeoutnumber
Request timeout in seconds. Default 30.
success_codesarray
Status codes treated as success. Default [200]. Any other status raises a recoverable tool error the agent can react to.

The tool returns {"content": ..., "status_code": ...}. A non-success status code becomes a recoverable observation — the agent sees the status and response text and can retry with different input or report the failure. The model can supply url, method, headers, params, and data per call, so one HTTP tool can serve a whole API — or you can pin everything in the configuration and expose a single fixed endpoint.

Custom Python tools

When no built-in tool fits, write one: the Custom Python Tool (python-tool) runs your Python function in a restricted sandbox. Attach it with Add tool, pick Custom Python Tool, and write the code in the Source Code editor. The contract:

  • The code must define a function named run. By default it is called as run(input_data) with a single dict containing every input field. With Use multiple params checked (in Advanced configuration; use_multiple_params in the SDK), the dict is unpacked into named arguments instead — run(amount, base, target) — and the builder parses the signature to create one input variable per parameter.
  • Imports are limited to an allowlist: base64, collections, copy, cmath, csv, datetime, dynamiq, functools, io, json, math, operator, pydantic, random, re, requests, statistics, time, typing, urllib, uuid, pandas, numpy, openpyxl, docx, pptx, pdfplumber, pypdf, matplotlib, seaborn, yaml. Anything else raises ImportError; relative imports are not supported, and attributes starting with _ are blocked.
  • Return a dict with a content key to control the tool's output exactly; any other return value is wrapped as {"content": <value>}. When the tool is attached to an agent, content is stringified into the observation the model reads. The return value is the only output — print is not available in the sandbox.
  • Exceptions become recoverable tool errors: the agent sees Code execution error: ... as an observation and can retry with different input.

A complete currency-converter tool:

import requests


def run(input_data):
    amount = float(input_data.get("amount", 1))
    base = str(input_data.get("base", "USD")).upper()
    target = str(input_data.get("target", "EUR")).upper()

    response = requests.get(
        f"https://api.frankfurter.dev/v1/latest?base={base}&symbols={target}",
        timeout=10,
    )
    response.raise_for_status()
    rate = response.json()["rates"][target]

    return {
        "content": f"{amount:.2f} {base} = {amount * rate:.2f} {target} (rate: {rate})"
    }

Because the agent's LLM only sees the tool's name and description — never the code — describe the expected fields explicitly, for example: "Converts an amount between currencies. Input fields: amount (number), base (3-letter currency code), target (3-letter currency code)."

Browser automation with Stagehand

The Stagehand tool (stagehand-browser) gives the agent a real remote browser it drives with natural-language instructions — for sites that have no API: JavaScript-rendered pages, form-driven portals, click-through flows. Each call performs one step, selected by action_type:

action_typeWhat it doesKey input
gotoNavigate to a URLurl
actPerform one page action — click, fill, selectinstruction
extractPull structured data described in plain languageinstruction
observeList candidate page elements for a planned interactioninstruction
uploadPerform an action that opens a file chooser, then upload the provided fileinstruction + files
go_backReturn to the previous page

Any extra input fields are forwarded to the underlying browser call — for example "iframes": true when the page nests content in iframes. The tool's built-in description teaches the model to split work into small single-action steps, so a task like "find the price of the top search result" plays out as a sequence of calls:

{"action_type": "goto", "brief": "Open the shop", "url": "https://shop.example.com"}
{"action_type": "act", "instruction": "Type 'mechanical keyboard' into the search field", "brief": "Enter the search query"}
{"action_type": "act", "instruction": "Press Enter on the search field", "brief": "Submit the search"}
{"action_type": "extract", "instruction": "Get the name and price of the first product in the results", "brief": "Extract the top result"}

Configuration: the browser session runs on a remote provider — in the UI, create a Stagehand Connection (Browserbase API key, Browserbase project ID, and a model API key for the LLM Stagehand uses to interpret instructions); the SDK additionally accepts a SteelBrowser connection for Steel cloud or self-hosted browsers. Set the model with model_name, and optionally enable screenshots captured after act, goto, go_back, and upload steps (is_return_screenshot_bytes_enabled) or a live view URL of the session (is_return_live_view_url_enabled). Each call returns content with the step's result, plus screenshot and live_view_url when enabled.

When to choose what: use Stagehand when the agent must interact with a website as a user; use the sandbox or a code tool when the job is data processing or the target has an API — scripted HTTP is faster and cheaper than driving a browser. For full desktop GUI control beyond the browser, there is the E2B Desktop Tool.

Pass runtime parameters with tool_params

Sometimes a tool needs a value that is only known at request time — a tenant id for a SQL query, a per-user API key, a feature flag. You should not make the LLM supply these (it may hallucinate them, and secrets must never enter the prompt). Instead, pass them in the agent's tool_params input: the values are merged into the tool's input at execution time and are never visible to the model.

Enable it in the UI with the Enable tool params checkbox in Advanced configuration, which adds a Tool params input field to the node, then map a value into it. The structure:

{
  "global": { "user_tenant": "acme" },
  "by_name": {
    "sql-executor": { "query_timeout": 30 }
  },
  "by_id": {
    "9f3a1c2e-tool-node-id": { "api_key": "value-from-secret-store" }
  }
}

Merging follows a fixed precedence, lowest to highest:

  1. global — applied to every tool call.
  2. by_name — applied when the tool's name (raw or sanitized) matches the key.
  3. by_id — applied when the tool node's id matches; overrides everything else.

Values are dictionaries merged into the tool's input; nested dict values are deep-merged key by key rather than replaced (and when both sides hold a list, the lists are concatenated). For sub-agents, a by_name/by_id entry can itself be a full tool_params object, which is forwarded to the child agent for its tools.

Worked example: per-user Knowledge Base filtering in a deployed App

A common production pattern: one deployed App serves many users, and each request must restrict the agent's Knowledge Base search to the calling user's documents. The agent has a Knowledge Base Retriever tool whose filters input accepts metadata conditions — but the caller, not the model, must control the filter.

Set it up once in the workflow:

  1. Attach the Knowledge Base with Add knowledge (this adds the Knowledge Base Retriever tool).
  2. Check Enable tool params in the agent's Advanced configuration — a Tool params field appears among the node's inputs.
  3. Map Tool params from a workflow input field (for example tool_params on the Input node), then deploy the workflow as an App.

Now every caller injects its own values at invoke time. The request below pins the retrieval filter to one tenant for every search-style tool, sets the result count for the Knowledge Base Retriever by name, and narrows one specific retriever node (by its node id) to a single department:

curl -X POST "https://<your-app-hostname>" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $DYNAMIQ_ACCESS_KEY" \
  -d '{
    "input": {
      "input": "What does our travel policy say about business-class flights?",
      "tool_params": {
        "global": {
          "filters": { "tenant_id": "acme" }
        },
        "by_name": {
          "knowledge-base-retriever": { "top_k": 5 }
        },
        "by_id": {
          "9f3a1c2e-tool-node-id": {
            "filters": { "department": "hr" }
          }
        }
      }
    },
    "stream": false
  }'

What happens on each retriever call in this run:

  • The model writes only the search query; it never sees tool_params.
  • global values merge into every tool call first — only put keys there that every attached tool accepts (or harmlessly ignores). Here every retrieval call gets filters.tenant_id = "acme".
  • The by_name entry matches the tool's name (knowledge-base-retriever is the default name Add knowledge assigns; if you renamed the tool, use your name). Raw and sanitized forms both match, so a name containing spaces works either way.
  • The by_id entry matches one tool node's id and wins last. Because filters exists in both layers and both values are dicts, they deep-merge: that node searches with tenant_id = "acme" and department = "hr". Use by_id when two attached tools share a name.
  • If the model had hallucinated a filters value, the injected one overrides it — the tenant boundary holds regardless of what the LLM writes.

The result: one workflow, one App, and per-request data isolation without a redeploy. The same pattern carries any caller-owned value — per-user API keys into an HTTP tool's headers, feature flags, or row-level SQL constraints.

In the SDK:

from dynamiq.connections import Http as HttpConnection
from dynamiq.connections import HTTPMethod
from dynamiq.connections import OpenAI as OpenAIConnection
from dynamiq.nodes.agents import Agent
from dynamiq.nodes.llms import OpenAI
from dynamiq.nodes.tools.http_api_call import HttpApiCall

crm_lookup = HttpApiCall(
    name="crm-lookup",
    description="Looks up a customer in the CRM by email.",
    connection=HttpConnection(
        url="https://crm.example.com/api/contacts",
        method=HTTPMethod.GET,
    ),
)

agent = Agent(
    name="support-agent",
    llm=OpenAI(connection=OpenAIConnection(), model="gpt-4o"),
    tools=[crm_lookup],
    role="You resolve support tickets using the CRM.",
)

result = agent.run(
    input_data={
        "input": "Why was order #1234 delayed?",
        "tool_params": {
            "by_name": {
                "crm-lookup": {"headers": {"X-Tenant": "acme"}}
            }
        },
    }
)
print(result.output["content"])

tool_params overrides whatever the model supplied for the same field. Use it for trusted, machine-provided values; if a parameter should be chosen by the model, put it in the tool's input schema and describe it instead.

Execution behavior

A few runtime details that affect how you design tool sets:

  • Caching — within a single run, calling the same tool with the exact same input returns the cached result instead of re-executing.
  • Parallel calls — with Enable parallel tool calls on, the model can request several tools in one step; eligible tools run concurrently, while tools flagged sequential-only run one at a time afterwards. Results come back as one combined observation, each marked SUCCESS or ERROR.
  • Failure handling — a recoverable tool error becomes an observation (ToolExecutionException: ...) that the model sees and can react to: retry with different input, switch tools, or report the failure in its answer. The run itself does not fail.
  • Output size — long tool outputs are truncated before being added to the conversation to protect the context window; enable the agent's File store if tools produce large artifacts the agent should keep.

Next steps

On this page