> ## Documentation Index
> Fetch the complete documentation index at: https://hub.hcompany.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Give an agent custom tools

> Extend the agent's toolbox with functions from your own code: the agent calls them, the SDK executes them, the run continues with the result.

export const CustomToolLoop = () => {
  const stroke = {
    fill: "none",
    stroke: "currentColor",
    strokeWidth: 1.75,
    strokeLinecap: "round",
    strokeLinejoin: "round"
  };
  const S = c => ({
    className: c,
    ...stroke
  });
  const icons = {
    agent: c => <svg viewBox="0 0 24 24" {...S(c)}><path d="M12 8V4H8" /><rect width="16" height="12" x="4" y="8" rx="2" /><path d="M2 14h2M20 14h2M15 13v2M9 13v2" /></svg>,
    code: c => <svg viewBox="0 0 24 24" {...S(c)}><path d="m16 18 6-6-6-6" /><path d="m8 6-6 6 6 6" /></svg>
  };
  const Card = ({icon, title, sub, children}) => <div className="flex shrink-0 flex-col self-center rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-950">
      <div className="flex items-center gap-2.5">
        <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-200">{icon("h-5 w-5")}</span>
        <div>
          <div className="whitespace-nowrap text-base font-semibold leading-6 text-zinc-900 dark:text-zinc-100">{title}</div>
          <div className="whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">{sub}</div>
        </div>
      </div>
      {children}
    </div>;
  const Arrow = ({top, bottom, dir = "right"}) => <div className="flex flex-col items-stretch justify-center gap-1 px-4 text-center text-xs leading-4 text-zinc-500 dark:text-zinc-400">
      {top && <span className="whitespace-nowrap">{top}</span>}
      <div className={`${dir === "left" ? "flex flex-row-reverse items-center text-zinc-400 dark:text-zinc-600" : "flex items-center text-zinc-400 dark:text-zinc-600"}`}>
        <span className="h-px flex-1 bg-current" />
        <svg className="-ml-px h-3 w-2 shrink-0" viewBox="0 0 8 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={dir === "left" ? {
    transform: "scaleX(-1)",
    marginLeft: 0,
    marginRight: -1
  } : undefined}>
          <path d="M1 1.5 6 6l-5 4.5" />
        </svg>
      </div>
      {bottom && <span className="whitespace-nowrap">{bottom}</span>}
    </div>;
  const Chip = ({children}) => <span className="whitespace-nowrap rounded-md bg-zinc-100 px-2 py-0.5 font-mono text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">{children}</span>;
  return <div className="not-prose my-8 overflow-x-auto">
      <div className="flex min-w-[620px] items-stretch justify-center">
        <Card icon={icons.agent} title="Agent" sub="in a session">
          <div className="mt-4 flex flex-col items-start gap-2 text-sm text-zinc-600 dark:text-zinc-400">
            <div>Needs a tool</div>
            <Chip>awaiting_tool_results</Chip>
            <div>Resumes once every call has a result</div>
          </div>
        </Card>

        <div className="flex min-w-[200px] flex-1 flex-col justify-center gap-6">
          <Arrow top="pending_tool_calls" />
          <Arrow bottom="POST /tool_results" dir="left" />
        </div>

        <Card icon={icons.code} title="Your code" sub="the SDK loop">
          <div className="mt-4 flex flex-col items-start gap-2 text-sm text-zinc-600 dark:text-zinc-400">
            <div>Match <span className="font-mono text-[13px]">tool_name</span></div>
            <div className="flex gap-1.5">
              <Chip>get_order_status()</Chip>
              <Chip>issue_refund()</Chip>
            </div>
            <div>Run it locally, echo <span className="font-mono text-[13px]">tool_req</span></div>
          </div>
        </Card>
      </div>
    </div>;
};

<CustomToolLoop />

Every agent works out of a toolbox. The [environment](/agents-api/environments/overview) supplies the core of it; custom tools are the part you add from your own code.

|            | Environment tools             | Custom tools                                                  |
| ---------- | ----------------------------- | ------------------------------------------------------------- |
| Examples   | Navigate, click, type, scroll | Query your database, call an internal API, look up a customer |
| Defined by | The environment               | You, as a function                                            |
| Runs       | In the environment            | In your process, via the SDK                                  |
| Agent sees | The screen                    | The return value                                              |

Pass a function to the SDK and the agent uses it like any other tool: when it decides to call one mid-run, the SDK executes your function locally and the run continues with the result. The full loop is handled for you.

## With the SDKs

Pass your functions via `tools`; the schema is derived from the signature and docstring in Python, or declared with `tool()` in TypeScript.

<CodeGroup>
  ```python Python theme={"system"}
  from hai_agents import Client

  def get_order_status(order_id: str) -> str:
      """Look up the status of an order in our system."""
      return db.orders.get(order_id).status

  client = Client()
  result = client.run_session(
      agent="h/web-surfer-flash",
      messages="Check order 4242 and email the customer if it shipped.",
      tools=[get_order_status],
  )
  print(result.answer)
  ```

  ```typescript TypeScript theme={"system"}
  import { HaiAgentsClient, tool } from "hai-agents";

  const getOrderStatus = tool({
    name: "get_order_status",
    description: "Look up the status of an order in our system.",
    inputSchema: {
      type: "object",
      properties: { order_id: { type: "string" } },
      required: ["order_id"],
    },
    fn: async ({ order_id }) => db.orders.get(order_id).status,
  });

  const client = new HaiAgentsClient();
  const result = await client.runSession({
    agent: "h/web-surfer-flash",
    messages: "Check order 4242 and email the customer if it shipped.",
    tools: [getOrderStatus],
  });
  console.log(result.answer);
  ```
</CodeGroup>

Functions may be sync or async, and exceptions are reported to the agent as tool errors instead of crashing the run.

Tools execute in the process that polls the session, so they only run while your program is waiting on `run_session` / `runSession` (or a handle's `wait_for_completion` / `waitForCompletion` with the same `tools`). Execution is at-least-once: if posting a result fails and the wait is retried, the tool may run again, so prefer idempotent tool functions for side-effecting operations.

## Over the raw API

Without an SDK to run the loop, you declare the tools, watch for the agent to call one, and post the result yourself.

<Steps titleSize="h3">
  <Step id="declare-tools" title="Declare the tools at session create">
    Declare the tools when [creating the session](/agents-api/sessions/create), inline on the agent or via the `agent.tools` override for a registered agent:

    ```json Session create body theme={"system"}
    {
      "agent": {
        "name": "support-agent",
        "environments": [{ "kind": "web" }],
        "tools": [
          {
            "name": "get_order_status",
            "description": "Look up the status of an order in our system.",
            "input_schema": {
              "type": "object",
              "properties": { "order_id": { "type": "string" } },
              "required": ["order_id"]
            }
          }
        ]
      },
      "messages": "Check order 4242 and email the customer if it shipped."
    }
    ```
  </Step>

  <Step id="detect-call" title="Detect the pending call">
    Long-poll [`changes`](/agents-api/sessions/changes) for an `ActiveStateChangeEvent` whose `data.state` is `"awaiting_tool_results"`. Its `data.pending_tool_calls` lists each pending call as a `{ tool_name, args, id }` object:

    ```json Pending tool call event theme={"system"}
    {
      "type": "ActiveStateChangeEvent",
      "data": {
        "state": "awaiting_tool_results",
        "pending_tool_calls": [
          { "tool_name": "get_order_status", "args": { "order_id": "4242" }, "id": "call_1" }
        ]
      },
      "timestamp": "2026-06-01T15:14:05Z"
    }
    ```
  </Step>

  <Step id="post-result" title="Post the result">
    Execute the call and [post the result](/agents-api/sessions/tool-results), echoing the pending call back as `tool_req`:

    ```bash theme={"system"}
    curl -X POST "https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/tool_results" \
      -H "Authorization: Bearer $HAI_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"kind": "tool_result", "tool_req": {"tool_name": "get_order_status", "args": {"order_id": "4242"}, "id": "call_1"}, "result": "shipped"}'
    ```

    Send several at once with `{"type": "batch", "results": [...]}`. Report a failure as an `error_event` instead of a `tool_result`; it carries `error`, `origin` (both required), and the echoed `tool_req`:

    ```json Tool error report theme={"system"}
    { "kind": "error_event", "error": "Order not found", "origin": "custom_tools", "tool_req": { "tool_name": "get_order_status", "args": { "order_id": "4242" }, "id": "call_1" } }
    ```

    The agent resumes once every pending call has a result. Calls still unresolved when the run ends (for example on `max_time_s`) fail with a model-visible error. Posting to a finished session returns `409`.
  </Step>
</Steps>

## Next steps

<CardGroup cols={2}>
  <Card title="Two-factor authentication" icon="shield-halved" href="/agents-api/two-factor-auth">
    A built-in tool for one-time codes.
  </Card>

  <Card title="Watch and steer sessions" icon="eye" href="/agents-api/observe-and-steer">
    See tool calls in the event stream.
  </Card>
</CardGroup>
