> ## 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.

# LangGraph

> Build stateful, graph-based AI workflows with StackOne tools and LangGraph

Build sophisticated, stateful AI workflows using LangGraph's graph-based architecture with easy access to business data through StackOne's Tools.

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

* **Stateful workflows** with persistent memory
* **Graph-based execution** with conditional branching
* **Multi-step processes** with human-in-the-loop
* **Dynamic tool loading** based on connected accounts

## Calling actions

<Steps>
  <Step title="Install">
    ```bash theme={null}
    # Requires Python 3.10 or higher
    # uv or pip

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

    # Using pip
    pip install 'stackone-ai[mcp]' langgraph langchain-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
    ```
  </Step>

  <Step title="Fetch Tools">
    Fetch StackOne tools for the linked account and convert them to LangChain format (LangGraph uses LangChain tools). By default this fetches every tool enabled for the account. Narrow it by passing `providers` or `actions` filters (see [Tool Filtering](/features/tool-discovery#tool-filtering)):

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

    # Get account ID from your app's auth context or StackOne dashboard
    account_id = "your-account-id"

    # Initialize toolset
    toolset = 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
    tools = toolset.fetch_tools(account_ids=[account_id])

    # Convert to LangChain format (LangGraph uses LangChain tools)
    langchain_tools = tools.to_langchain()
    ```
  </Step>

  <Step title="Run the agent loop">
    Create a LangGraph agent with the tools and run a stateful workflow:

    ```python theme={null}
    from langgraph.prebuilt import create_react_agent
    from langchain_openai import ChatOpenAI

    # Create LangGraph agent
    model = ChatOpenAI(model="gpt-5.4")
    agent = create_react_agent(model, langchain_tools)

    # Run stateful workflow
    config = {"configurable": {"thread_id": "user-123-session"}}
    result = agent.invoke(
        {"messages": [("user", "Find all employees hired this year")]},
        config=config
    )
    ```
  </Step>
</Steps>

## Stateful workflows

Build complex, multi-step workflows with state:

```python theme={null}
from langgraph.graph import StateGraph, MessagesState
from langgraph.prebuilt import ToolNode
from langchain_core.messages import SystemMessage

# Define workflow state
class AgentState(MessagesState):
    employee_data: dict
    analysis_complete: bool

# Create graph
def create_hr_workflow(account_id: str):
    toolset = StackOneToolSet()
    tools = toolset.fetch_tools(account_ids=[account_id])
    langchain_tools = tools.to_langchain()

    # Create nodes
    tool_node = ToolNode(langchain_tools)

    def agent_node(state: AgentState):
        model = ChatOpenAI(model="gpt-5.4")
        model_with_tools = model.bind_tools(langchain_tools)

        messages = [
            SystemMessage(content="You are an HR data analyst."),
            *state["messages"]
        ]
        response = model_with_tools.invoke(messages)
        return {"messages": [response]}

    # Build graph
    workflow = StateGraph(AgentState)
    workflow.add_node("agent", agent_node)
    workflow.add_node("tools", tool_node)

    workflow.set_entry_point("agent")
    workflow.add_conditional_edges("agent", should_continue)
    workflow.add_edge("tools", "agent")

    return workflow.compile()

def should_continue(state: AgentState):
    """Decide whether to continue or finish"""
    last_message = state["messages"][-1]
    if not last_message.tool_calls:
        return "end"
    return "tools"
```

## Human-in-the-loop

Add human approval for sensitive operations:

```python theme={null}
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver

def create_hr_agent_with_approval(account_id: str):
    toolset = StackOneToolSet()

    # Only expose read operations initially (optional filtering)
    read_tools = toolset.fetch_tools(
        account_ids=[account_id],
        actions=["*_list_*", "*_get_*"]  # Only list and get operations
    )

    agent = create_react_agent(
        model=ChatOpenAI(model="gpt-5.4"),
        tools=read_tools.to_langchain(),
        checkpointer=MemorySaver()
    )

    return agent

# Usage: Approve before executing sensitive actions
agent = create_hr_agent_with_approval(account_id)
result = agent.invoke({"messages": [("user", "Review termination candidates")]})

# Human reviews, then loads write tools for approved actions
if user_approves(result):
    write_tools = toolset.fetch_tools(
        account_ids=[account_id],
        actions=["*_update_*", "*_create_*"]  # Only update and create operations
    )
    # Continue with write operations...
```

## Best practices

### Account ID from context

```python theme={null}
# Get from request context
account_id = get_account_from_auth_token(request)
account_id = tenant.stackone_account_id
```

### Multi-account usage

Each tool execution runs against a specific customer account. Set account IDs once on the toolset, or pass them per request.

```python theme={null}
# Set accounts on the toolset for all subsequent calls
toolset.set_accounts(["account-123", "account-456"])
tools = toolset.fetch_tools()

# Or pass account IDs per request
tools = toolset.fetch_tools(account_ids=["account-123", "account-456"])

# Loop over customer accounts dynamically
customer_accounts = ["account-1", "account-2", "account-3"]

for account_id in customer_accounts:
    tools = toolset.fetch_tools(
        actions=["workday_list_workers"],
        account_ids=[account_id],
    )
    employee_tool = tools.get_tool("workday_list_workers")
    if employee_tool:
        employees = employee_tool.call()
        print(f"Found {len(employees['data'])} employees")
```

<AccordionGroup>
  <Accordion title="Error handling">
    ```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.

    `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.

```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)
```

## 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>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.