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

# OpenAI

> Build AI agents with OpenAI function calling using the StackOne Agent SDK for TypeScript or Python

export const GitHubCode = ({url, lang, title, children}) => {
  const urlMatch = url?.match(/https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/(.+?)(?:#L(\d+)(?:-L(\d+))?)?$/);
  const owner = urlMatch?.[1];
  const repo = urlMatch?.[2];
  const branch = urlMatch?.[3];
  const filePath = urlMatch?.[4];
  const startLine = urlMatch?.[5];
  const endLine = urlMatch?.[6];
  const getDisplayTitle = () => {
    if (title) return title;
    if (!filePath) return 'source';
    const parts = filePath.split('/');
    if (parts.length >= 2) {
      return parts.slice(-2).join('/');
    }
    return parts[parts.length - 1];
  };
  const displayPath = filePath || 'source';
  const displayTitle = getDisplayTitle();
  const lineInfo = startLine ? endLine ? `#L${startLine}-L${endLine}` : `#L${startLine}` : '';
  const fullUrl = url + (lineInfo && !url.includes('#L') ? lineInfo : '');
  const encodedUrl = encodeURIComponent(fullUrl);
  const iframeSrc = `https://emgithub.com/iframe.html?target=${encodedUrl}&style=github&type=code&showBorder=on&showLineNumbers=on&showFileMeta=on`;
  return <div style={{
    marginTop: '1.5rem',
    marginBottom: '1.5rem',
    border: '1px solid #e5e7eb',
    borderRadius: '0.75rem',
    overflow: 'hidden',
    boxShadow: '0 1px 3px 0 rgb(0 0 0 / 0.1)'
  }}>
      <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: '1rem 1.25rem',
    backgroundColor: '#f9fafb',
    borderBottom: '1px solid #e5e7eb'
  }}>
        <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '0.75rem'
  }}>
          <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" style={{
    flexShrink: 0
  }}>
            <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
          </svg>
          <span style={{
    fontWeight: 600,
    fontSize: '0.9375rem'
  }}>{displayTitle}</span>
        </div>
        {url && <a href={url} target="_blank" rel="noopener noreferrer" style={{
    fontSize: '0.875rem',
    color: '#3b82f6',
    textDecoration: 'none',
    fontWeight: 500
  }}>
            View on GitHub →
          </a>}
      </div>
      <div style={{
    padding: '0.5rem',
    minHeight: '400px',
    backgroundColor: '#ffffff'
  }}>
        {children || <iframe src={iframeSrc} style={{
    width: '100%',
    height: '420px',
    border: 'none',
    borderRadius: '0.25rem'
  }} loading="lazy" />}
      </div>
    </div>;
};

StackOne tools work with OpenAI's function calling to build AI agents that access business data.

<Note>
  **Supported languages:** TypeScript and Python.
</Note>

* **Convert tools** to OpenAI function schemas automatically
* **Execute functions** with built-in handling
* **Build conversational agents** with tool execution
* **Multi-step workflows** for complex tasks

## Calling actions

<Steps>
  <Step title="Install">
    <CodeGroup>
      ```bash TypeScript theme={null}
      # Requires Node.js 16.0 or higher
      # TypeScript 4.5 or higher (recommended)

      # Using npm
      npm install @stackone/ai openai

      # Using pnpm
      pnpm add @stackone/ai openai

      # Using yarn
      yarn add @stackone/ai openai

      # Using bun
      bun add @stackone/ai openai

      export STACKONE_API_KEY=your_api_key_here
      ```

      ```bash Python theme={null}
      # Requires Python 3.10 or higher

      # Using uv (recommended)
      uv add 'stackone-ai[mcp]' openai

      # Using pip
      pip install 'stackone-ai[mcp]' openai

      # Optional: install with example dependencies
      uv add 'stackone-ai[mcp,examples]'
      pip install 'stackone-ai[mcp,examples]'

      export STACKONE_API_KEY=your_api_key_here
      ```
    </CodeGroup>
  </Step>

  <Step title="Fetch Tools">
    Two approaches to retrieving tools:

    1. [Fetch Tools (with filtering)](/features/tool-discovery#tool-filtering)  — fetch all tools upfront with optional filters applied.
    2. [Search & Execute](/features/tool-discovery#search-and-execute) — hand the model just `tool_search` + `tool_execute` and let the agent discover what it needs at runtime.

    <Note>
      See [Tool Discovery](/features/tool-discovery) for more details on which may be best in your scenario.
    </Note>

    <Tabs>
      <Tab title="Fetch tools">
        By default this fetches every tool enabled for the account. Narrow it by passing `providers` (specific connectors) or `actions` — exact names or glob patterns, following the `provider_operation_entity` tool naming (e.g. `workday_*`, or `*_list_*` for read-only tools).

        <CodeGroup>
          ```typescript TypeScript theme={null}
          import OpenAI from 'openai';
          import { StackOneToolSet } from '@stackone/ai';

          // Get account ID from your app's auth context or StackOne dashboard
          const accountId = 'your-account-id';

          const openai = new OpenAI();
          const toolset = new StackOneToolSet();

          // Fetch tools dynamically for this account.
          // Optionally narrow the fetch with filters:
          //   providers: ["hibob", "workday"]    — only these connectors
          //   actions: ["*_list_*", "*_get_*"]   — glob patterns, e.g. read-only actions
          const tools = await toolset.fetchTools({
            accountIds: [accountId]
          });

          // Convert to OpenAI function format
          const openAITools = tools.toOpenAI();
          ```

          ```python Python theme={null}
          from openai import OpenAI
          from stackone_ai import StackOneToolSet

          # Initialise clients
          client = OpenAI()
          toolset = StackOneToolSet()

          # Fetch StackOne tools.
          # Optionally narrow the fetch with filters:
          #   providers=["hibob", "workday"]    — only these connectors
          #   actions=["*_list_*", "*_get_*"]   — glob patterns, e.g. read-only actions
          tools = toolset.fetch_tools(
              actions=['workday_get_worker', 'workday_list_workers'],
              account_ids=['your-account-id']
          )

          # Convert to OpenAI function format
          openai_tools = tools.to_openai()
          ```
        </CodeGroup>
      </Tab>

      <Tab title="Search & execute">
        The LLM receives only 2 tools and searches the catalog autonomously:

        <CodeGroup>
          ```typescript TypeScript theme={null}
          import { StackOneToolSet } from '@stackone/ai';

          const toolset = new StackOneToolSet({
            search: { method: 'semantic', topK: 3 },
          });

          // tools contains only tool_search + tool_execute
          const tools = toolset.getTools({ accountIds: ['your-account-id'] });

          // Convert to OpenAI function format — the agent loop also uses
          // `tools` to execute the calls the model makes
          const openAITools = tools.toOpenAI();
          ```

          ```python Python theme={null}
          from stackone_ai import StackOneToolSet

          toolset = StackOneToolSet(
              search={"method": "semantic", "top_k": 3},
              execute={"account_ids": ["your-account-id"]},
          )

          # LLM receives only 2 tools - it searches and executes autonomously.
          # .openai() returns OpenAI-format tools directly, so no conversion is needed
          openai_tools = toolset.openai(mode="search_and_execute")
          ```
        </CodeGroup>
      </Tab>
    </Tabs>
  </Step>

  <Step title="Run the agent loop">
    <Tabs>
      <Tab title="Fetch tools">
        Create the completion with the tools attached, then execute any tool calls the model makes:

        <CodeGroup>
          ```typescript TypeScript theme={null}
          // Create completion with function calling
          const completion = await openai.chat.completions.create({
            model: 'gpt-5.4',
            messages: [
              {
                role: 'system',
                content: 'You are an HR assistant with access to employee data.'
              },
              {
                role: 'user',
                content: 'How many employees do we have?'
              }
            ],
            tools: openAITools,
            tool_choice: 'auto'
          });

          // Execute any tool calls
          const message = completion.choices[0].message;
          if (message.tool_calls) {
            for (const toolCall of message.tool_calls) {
              const tool = tools.getTool(toolCall.function.name);
              if (tool) {
                const result = await tool.execute(
                  JSON.parse(toolCall.function.arguments)
                );
                console.log('Result:', result.data);
              }
            }
          }
          ```

          ```python Python theme={null}
          # Create an AI agent with tool access
          response = client.chat.completions.create(
              model="gpt-5.4",
              messages=[
                  {
                      "role": "system",
                      "content": "You are an HR assistant with access to employee data."
                  },
                  {
                      "role": "user",
                      "content": "How many employees are in engineering?"
                  }
              ],
              tools=openai_tools,
              tool_choice="auto"
          )

          # Handle tool calls
          message = response.choices[0].message
          if message.tool_calls:
              for tool_call in message.tool_calls:
                  tool = tools.get_tool(tool_call.function.name)
                  if tool:
                      result = tool.execute(tool_call.function.arguments)
                      print(f"Result: {result}")
          ```
        </CodeGroup>
      </Tab>

      <Tab title="Search & execute">
        The model first calls `tool_search`, then `tool_execute` — the same loop as the Fetch tools tab, run until the model stops calling tools:

        <CodeGroup>
          ```typescript TypeScript theme={null}
          // Route every tool call through the toolset and feed the result
          // back until the model stops
          import OpenAI from 'openai';

          const client = new OpenAI();

          const messages: OpenAI.ChatCompletionMessageParam[] = [
            {
              role: 'system',
              content:
                'Use tool_search to find relevant tools, then tool_execute to run them. ' +
                'Read parameter schemas from tool_search results carefully.',
            },
            { role: 'user', content: 'List my upcoming Calendly events for the next week.' },
          ];

          for (let step = 0; step < 10; step++) {
            const response = await client.chat.completions.create({
              model: 'gpt-5.4',
              messages,
              tools: openAITools,
              tool_choice: 'auto',
            });

            const choice = response.choices[0];
            if (!choice.message.tool_calls?.length) {
              console.log(choice.message.content);
              break;
            }

            messages.push(choice.message);
            for (const toolCall of choice.message.tool_calls) {
              // Look the meta tool up in the collection from the fetch step
              const tool = tools.getTool(toolCall.function.name);
              const result = tool
                ? await tool.execute(toolCall.function.arguments)
                : { error: `Tool "${toolCall.function.name}" not found` };
              messages.push({
                role: 'tool',
                tool_call_id: toolCall.id,
                content: JSON.stringify(result),
              });
            }
          }
          ```

          ```python Python theme={null}
          # Route every tool call through the toolset and feed the result
          # back until the model stops
          import json
          from openai import OpenAI

          client = OpenAI()

          messages = [
              {
                  "role": "system",
                  "content": (
                      "Use tool_search to find relevant tools, then tool_execute to run them. "
                      "Read parameter schemas from tool_search results carefully."
                  ),
              },
              {"role": "user", "content": "List my upcoming Calendly events for the next week."},
          ]

          for _step in range(10):
              response = client.chat.completions.create(
                  model="gpt-5.4",
                  messages=messages,
                  tools=openai_tools,
                  tool_choice="auto",
              )

              choice = response.choices[0]
              if not choice.message.tool_calls:
                  print(choice.message.content)
                  break

              messages.append(choice.message.model_dump(exclude_none=True))
              for tool_call in choice.message.tool_calls:
                  result = toolset.execute(tool_call.function.name, tool_call.function.arguments)
                  messages.append({
                      "role": "tool",
                      "tool_call_id": tool_call.id,
                      "content": json.dumps(result),
                  })
          ```
        </CodeGroup>
      </Tab>
    </Tabs>
  </Step>
</Steps>

## Example

A full runnable example:

<Tabs>
  <Tab title="TypeScript" icon="js">
    <GitHubCode url="https://github.com/StackOneHQ/stackone-ai-node/blob/main/examples/openai-integration.ts" lang="typescript">
      <iframe src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2FStackOneHQ%2Fstackone-ai-node%2Fblob%2Fmain%2Fexamples%2Fopenai-integration.ts&style=github&type=code&showBorder=on&showLineNumbers=on&showFileMeta=on" style={{ width: '100%', height: '400px', border: 'none' }} loading="lazy" />
    </GitHubCode>
  </Tab>

  <Tab title="Python" icon="python">
    <GitHubCode url="https://github.com/StackOneHQ/stackone-ai-python/blob/main/examples/openai_integration.py" lang="python">
      <iframe src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2FStackOneHQ%2Fstackone-ai-python%2Fblob%2Fmain%2Fexamples%2Fopenai_integration.py&style=github&type=code&showBorder=on&showLineNumbers=on&showFileMeta=on" style={{ width: '100%', height: '400px', border: 'none' }} loading="lazy" />
    </GitHubCode>
  </Tab>
</Tabs>

<AccordionGroup>
  <Accordion title="Error handling (Python)">
    ```python theme={null}
    from stackone_ai.models import StackOneError, StackOneAPIError

    try:
        result = employee_tool.call(id="employee-123")
        print("Success:", result)
    except StackOneAPIError as e:
        print(f"API error: {e.message}")
    except StackOneError as e:
        print(f"StackOne error: {e.message}")
    except Exception as e:
        print(f"Unexpected error: {e}")
    ```
  </Accordion>

  <Accordion title="File downloads (binary responses)">
    Actions that download a file (for example `googledrive_unified_download_file`, `documents_download_file`, or any `*_unified_download_file`) return raw bytes plus metadata, not parsed JSON. The SDK decides from the response `Content-Type`: JSON is parsed as usual, and anything else is treated as a file download.

    **TypeScript** — because `execute()` is typed to return a JSON object, use the exported `isBinaryDownloadResult` guard to narrow the result to the file shape.

    ```typescript theme={null}
    import { writeFileSync } from "node:fs";
    import { isBinaryDownloadResult } from "@stackone/ai";

    const tools = await toolset.fetchTools({
      actions: ["googledrive_*"],
      accountIds: ["your-account-id"],
    });
    const download = tools.getTool("googledrive_unified_download_file");

    if (download) {
      const result = await download.execute({ id: "file-id" });

      // isBinaryDownloadResult narrows result so content is a Buffer (no cast) and
      // confirms this was a file download rather than a JSON response.
      if (isBinaryDownloadResult(result)) {
        writeFileSync(result.fileName ?? "download.bin", result.content);
      }
    }
    ```

    The narrowed `result` describes the file:

    | Key           | Type             | Description                                                                    |
    | ------------- | ---------------- | ------------------------------------------------------------------------------ |
    | `content`     | `Buffer`         | Raw file bytes. Not JSON-serializable (see note).                              |
    | `contentType` | `string`         | File MIME type (for example `application/pdf`), or `application/octet-stream`. |
    | `statusCode`  | `number`         | HTTP status of the download response.                                          |
    | `headers`     | `object`         | Response headers.                                                              |
    | `fileName`    | `string \| null` | Filename from `Content-Disposition` (RFC 5987 `filename*` aware), else `null`. |

    **Python** — `call()` and `execute()` both return the dict directly (it isn't wrapped in a result object), so read its values with dict keys like `result["content"]`.

    ```python theme={null}
    tools = toolset.fetch_tools(actions=["googledrive_*"], account_ids=["your-account-id"])
    download = tools.get_tool("googledrive_unified_download_file")

    result = download.execute({"id": "file-id"})

    # result describes the file. Write the bytes straight to disk:
    with open(result["file_name"] or "download.bin", "wb") as f:
        f.write(result["content"])
    ```

    The returned dict, from both `call()` and `execute()`:

    | Key            | Type          | Description                                                                    |
    | -------------- | ------------- | ------------------------------------------------------------------------------ |
    | `content`      | `bytes`       | Raw file bytes. Not JSON-serializable (see note).                              |
    | `content_type` | `str`         | File MIME type (for example `application/pdf`), or `application/octet-stream`. |
    | `status_code`  | `int`         | HTTP status of the download response.                                          |
    | `headers`      | `dict`        | Response headers.                                                              |
    | `file_name`    | `str \| None` | Filename from `Content-Disposition` (RFC 5987 `filename*` aware), else `None`. |

    <Warning>
      `content` is raw bytes and is not JSON-serializable. If you forward tool results to an LLM (or anything that re-serializes to JSON), handle or strip the `content` key. For example, base64-encode it on the LLM-facing path.
    </Warning>
  </Accordion>
</AccordionGroup>

## Troubleshooting

### Execute a tool

Direct execution is useful for testing and debugging. In production, your agent framework handles tool calls automatically.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const tools = await toolset.fetchTools({ accountIds: ["your-account-id"] });
  const employeeTool = tools.getTool("workday_list_workers");

  if (employeeTool) {
    const result = await employeeTool.execute({
      query: { limit: 10 },
    });
    console.log(result);
  }
  ```

  ```python Python theme={null}
  tools = toolset.fetch_tools(account_ids=["your-account-id"])
  employee_tool = tools.get_tool("workday_list_workers")

  # call() with keyword arguments
  result = employee_tool.call(id="employee-123", include_details=True)
  print(result["data"])

  # execute() with dictionary payloads
  result = employee_tool.execute({"id": "employee-123", "include_details": True})

  # execute() with OpenAI-style arguments
  payload = {"arguments": {"id": "employee-123"}}
  result = employee_tool.execute(payload)
  ```
</CodeGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Observability" href="/features/observability/overview" icon="chart-line">
    Diagnose failing calls and monitor what your agent runs.
  </Card>

  <Card title="Tool Defense" href="/features/tool-defense" icon="shield-halved">
    Protect your agent from malicious content in tool results.
  </Card>
</CardGroup>
