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