StackOne AI provides a unified interface for accessing various SaaS tools through AI-friendly APIs.
- Unified interface for multiple SaaS tools
- AI-friendly tool descriptions and parameters
- Tool Calling: Direct method calling with
tool.call()for intuitive usage - MCP-backed Dynamic Discovery: Fetch tools at runtime via
fetch_tools()with provider, action, and account filtering - Advanced Tool Filtering:
- Glob pattern filtering with patterns like
"salesforce_*"and exclusions"!*_delete_*" - Provider and action filtering
- Multi-account support
- Glob pattern filtering with patterns like
- Semantic Search: AI-powered tool discovery using natural language queries
- Search Tool: Callable tool discovery for agent loops via
get_search_tool() - Integration with popular AI frameworks:
- OpenAI Functions
- LangChain Tools
- CrewAI Tools
- LangGraph Tool Node
- Pydantic AI Toolset
- Python 3.10+
pip install 'stackone-ai[mcp]'# Or with uv
uv add 'stackone-ai[mcp]'# Install with CrewAI examples
pip install 'stackone-ai[mcp,examples]'# or
uv add 'stackone-ai[mcp,examples]'importosfromstackone_aiimportStackOneToolSet# Initialize — reads STACKONE_API_KEY from environmenttoolset=StackOneToolSet()
# Fetch tools — pass account ID from STACKONE_ACCOUNT_ID env varaccount_id=os.getenv("STACKONE_ACCOUNT_ID")
tools=toolset.fetch_tools(actions=["workday_*"], account_ids=[account_id])
# Use a specific tool with the call methodemployee_tool=tools.get_tool("workday_get_worker")
# Call with keyword argumentsemployee=employee_tool.call(id="employee-id")
# Or with traditional execute methodemployee=employee_tool.execute({"id": "employee-id"})StackOne AI SDK provides powerful filtering capabilities to help you select the exact tools you need.
The fetch_tools() method provides filtering by providers, actions, and account IDs:
fromstackone_aiimportStackOneToolSettoolset=StackOneToolSet()
# Filter by account IDstools=toolset.fetch_tools(account_ids=["acc-123", "acc-456"])
# Filter by providers (case-insensitive)tools=toolset.fetch_tools(providers=["hibob", "workday"])
# Filter by action patterns with glob supporttools=toolset.fetch_tools(actions=["*_list_employees"])
# Combine multiple filterstools=toolset.fetch_tools(
account_ids=["acc-123"],
providers=["hibob"],
actions=["*_list_*"]
)
# Use set_accounts() for chainingtoolset.set_accounts(["acc-123", "acc-456"])
tools=toolset.fetch_tools(providers=["hibob"])Filtering Options:
account_ids: Filter tools by account IDs. Tools will be loaded for each specified account.providers: Filter by provider names (e.g.,["hibob", "workday"]). Case-insensitive matching.actions: Filter by action patterns with glob support:- Exact match:
["workday_list_workers"] - Glob pattern:
["*_list_employees"]matches all tools ending with_list_employees - Provider prefix:
["workday_*"]matches all Workday tools
- Exact match:
Actions that return a file — e.g. googledrive_unified_download_file, documents_download_file, any *_unified_download_file — resolve to raw bytes plus metadata, not parsed JSON. The SDK decides this from the response Content-Type: a JSON content type is parsed as before; anything else is treated as a file download. This applies to both tool.execute() and tool.call().
tools=toolset.fetch_tools(actions=["googledrive_*"], account_ids=[account_id])
download=tools.get_tool("googledrive_unified_download_file")
result=download.execute({"id": "file-id"})
# `result` is a dict describing the file — write the bytes straight to disk:withopen(result["file_name"] or"download.bin", "wb") asf:
f.write(result["content"])The returned dict:
| Key | Type | Description |
|---|---|---|
content | bytes | Raw file bytes. Not JSON-serializable — see the caveat below. |
content_type | str | The file's MIME type (e.g. application/pdf), or application/octet-stream if unspecified. |
status_code | int | HTTP status of the download response. |
headers | dict | Response headers. |
file_name | str | None | Filename from the Content-Disposition header (handles RFC 5987 filename*), else None. |
Caveat:
contentholds raw bytes, which are not JSON-serializable. If you forward tool results to an LLM — or anywhere that re-serializes them to JSON — handle or strip thecontentkey (for example, base64-encode it on the LLM-facing path).
JSON responses are unchanged: any action returning application/json (or a …+json type) is parsed and returned as a dict exactly as before.
The Python SDK can emit implicit behavioral feedback to LangSmith so you can triage low-quality tool results without manually tagging runs.
Set LANGSMITH_API_KEY in your environment and the SDK will initialize the implicit feedback manager on first tool execution. You can optionally fine-tune behavior with:
STACKONE_IMPLICIT_FEEDBACK_ENABLED(true/false, defaults totruewhen an API key is present)STACKONE_IMPLICIT_FEEDBACK_PROJECTto pin a LangSmith project nameSTACKONE_IMPLICIT_FEEDBACK_TAGSwith a comma-separated list of tags applied to every run
If you want custom session or user resolvers, call configure_implicit_feedback during start-up:
fromstackone_aiimportconfigure_implicit_feedbackconfigure_implicit_feedback(
api_key="/path/to/langsmith.key",
project_name="stackone-agents",
default_tags=["python-sdk"],
)Providing your own session_resolver/user_resolver callbacks lets you derive identifiers from the request context before events are sent to LangSmith.
Both tool.execute and tool.call accept an options keyword that is excluded from the API request but forwarded to the feedback manager:
tool.execute(
{"id": "employee-id"},
options={
"feedback_session_id": "chat-42",
"feedback_user_id": "user-123",
"feedback_metadata": {"conversation_id": "abc"},
},
)When two calls for the same session happen within a few seconds, the SDK emits a refinement_needed event, and you can inspect suitability scores directly in LangSmith.
LangChain Integration
StackOne tools work seamlessly with LangChain, enabling powerful AI agent workflows:
importosfromlangchain_openaiimportChatOpenAIfromstackone_aiimportStackOneToolSet# Initialize StackOne toolstoolset=StackOneToolSet()
account_id=os.getenv("STACKONE_ACCOUNT_ID")
tools=toolset.fetch_tools(actions=["workday_*"], account_ids=[account_id])
# Convert to LangChain formatlangchain_tools=tools.to_langchain()
# Use with LangChain modelsmodel=ChatOpenAI(model="gpt-5.4")
model_with_tools=model.bind_tools(langchain_tools)
# Execute AI-driven tool callsresponse=model_with_tools.invoke("Get employee information for ID: emp123")
# Handle tool callsfortool_callinresponse.tool_calls:
tool=tools.get_tool(tool_call["name"])
iftool:
result=tool.execute(tool_call["args"])
print(f"Result: {result}")Pydantic AI Integration
StackOne tools convert to Pydantic AI Tool instances via .to_pydantic_ai(), parallel to .to_openai() and .to_langchain():
Prerequisites:
pip install 'stackone-ai[pydantic-ai]'importosfrompydantic_aiimportAgentfromstackone_aiimportStackOneToolSettoolset=StackOneToolSet()
tools=toolset.fetch_tools(
actions=["workday_list_workers", "workday_get_worker"],
account_ids=[os.environ["STACKONE_ACCOUNT_ID"]],
).to_pydantic_ai()
agent=Agent("openai:gpt-5.4", tools=tools)
result=agent.run_sync("List the first 5 employees")
print(result.output)For the full catalog (or the meta search/execute tools), use the .pydantic_ai() method on StackOneToolSet — parallel to .openai() / .langchain():
toolset=StackOneToolSet()
tools=toolset.pydantic_ai(account_ids=[os.environ["STACKONE_ACCOUNT_ID"]])
# For agent-driven discovery, enable search on the constructor:# toolset = StackOneToolSet(search={"method": "auto"})# tools = toolset.pydantic_ai(mode="search_and_execute")LangGraph Integration
StackOne tools convert to LangChain tools, which LangGraph consumes via its prebuilt nodes:
Prerequisites:
pip install langgraph langchain-openaiimportosfromlangchain_openaiimportChatOpenAIfromtypingimportAnnotatedfromtyping_extensionsimportTypedDictfromlanggraph.graphimportStateGraph, START, ENDfromlanggraph.graph.messageimportadd_messagesfromlanggraph.prebuiltimporttools_conditionfromstackone_aiimportStackOneToolSetfromstackone_ai.integrations.langgraphimportto_tool_node, bind_model_with_tools# Prepare toolstoolset=StackOneToolSet()
account_id=os.getenv("STACKONE_ACCOUNT_ID")
tools=toolset.fetch_tools(actions=["workday_*"], account_ids=[account_id])
langchain_tools=tools.to_langchain()
classState(TypedDict):
messages: Annotated[list, add_messages]
# Build a small agent loop: LLM -> maybe tools -> back to LLMgraph=StateGraph(State)
graph.add_node("tools", to_tool_node(langchain_tools))
defcall_llm(state: dict):
llm=ChatOpenAI(model="gpt-5.4")
llm=bind_model_with_tools(llm, langchain_tools)
resp=llm.invoke(state["messages"]) # returns AIMessage with optional tool_callsreturn {"messages": state["messages"] + [resp]}
graph.add_node("llm", call_llm)
graph.add_edge(START, "llm")
graph.add_conditional_edges("llm", tools_condition)
graph.add_edge("tools", "llm")
app=graph.compile()
_=app.invoke({"messages": [("user", "Get employee with id emp123") ]})CrewAI Integration
CrewAI uses LangChain tools natively, making integration seamless:
importosfromcrewaiimportAgent, Crew, Taskfromstackone_aiimportStackOneToolSet# Get tools and convert to LangChain formattoolset=StackOneToolSet()
account_id=os.getenv("STACKONE_ACCOUNT_ID")
tools=toolset.fetch_tools(actions=["workday_*"], account_ids=[account_id])
langchain_tools=tools.to_langchain()
# Create CrewAI agent with StackOne toolsagent=Agent(
role="HR Manager",
goal="Analyze employee data and generate insights",
backstory="Expert in HR analytics and employee management",
tools=langchain_tools,
llm="gpt-5.4"
)
# Define task and executetask=Task(
description="Find all employees in the engineering department",
agent=agent,
expected_output="List of engineering employees with their details"
)
crew=Crew(agents=[agent], tasks=[task])
result=crew.kickoff()The SDK includes a feedback collection tool (tool_feedback) that allows users to submit feedback about their experience with StackOne tools. This tool is automatically included in the toolset and is designed to be invoked by AI agents after user permission.
fromstackone_aiimportStackOneToolSettoolset=StackOneToolSet()
# Get the feedback tool (included with "tool_*" pattern or all tools)tools=toolset.fetch_tools(actions=["tool_*"])
feedback_tool=tools.get_tool("tool_feedback")
# Submit feedback (typically invoked by AI after user consent)result=feedback_tool.call(
feedback="The HRIS tools are working great! Very fast response times.",
account_id="acc_123456",
tool_names=["workday_list_workers", "workday_get_worker"]
)Important: The AI agent should always ask for user permission before submitting feedback:
- "Are you ok with sending feedback to StackOne? The LLM will take care of sending it."
- Only call the tool after the user explicitly agrees.
Search for tools using natural language queries. Works with both semantic (cloud) and local BM25+TF-IDF search.
importosfromstackone_aiimportStackOneToolSet# Get a callable search tool — search must be enabled on the toolsettoolset=StackOneToolSet(search={"method": "auto"})
account_id=os.getenv("STACKONE_ACCOUNT_ID")
search_tool=toolset.get_search_tool()
# Search for relevant tools — returns a Tools collection scoped to the accounttools=search_tool("manage employees", top_k=5, account_ids=[account_id])
# Execute a discovered tool directlytools[0](limit=10)Discover tools using natural language instead of exact names. Queries like "onboard new hire" resolve to the right actions even when the tool is called workday_create_employee.
importosfromstackone_aiimportStackOneToolSet# Search must be enabled on the constructor — pass `search={}` for defaults,# or set a backend / top_k explicitly.toolset=StackOneToolSet(search={"method": "auto"})
# Search by intent — returns Tools collection ready for any frameworkaccount_id=os.getenv("STACKONE_ACCOUNT_ID")
tools=toolset.search_tools("manage employee records", account_ids=[account_id], top_k=5)
openai_tools=tools.to_openai()
# Lightweight: inspect results without fetching full tool definitionsresults=toolset.search_action_names("time off requests", top_k=5)Control which search backend search_tools() uses via the search parameter:
# "auto" (default) — tries semantic search first, falls back to localtools=toolset.search_tools("manage employees", search="auto")
# "semantic" — semantic API only, raises if unavailabletools=toolset.search_tools("manage employees", search="semantic")
# "local" — local BM25+TF-IDF only, no semantic API calltools=toolset.search_tools("manage employees", search="local")Results are automatically scoped to connectors in your linked accounts. See Search Tools Example for SearchTool (get_search_tool) integration, OpenAI, and LangChain patterns.
For more examples, check out the examples/ directory:
- OpenAI Integration — OpenAI function calling
- LangChain Integration — LangChain tools
- CrewAI Integration — CrewAI agent
- Search Tools — Tool discovery (semantic, local, auto search)
- Auth Management — API key and account ID patterns
# 1. Set up credentials
cp .env.example .env
# Edit .env with your API keys# 2. Install dependencies
uv sync --all-extras
# 3. Run any example
uv run examples/search_tools.pyThis project includes a Nix flake for reproducible development environments. All development tools are defined in flake.nix and provided via Nix.
# Install Nix with flakes enabled (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf -L https://artifacts.nixos.org/experimental-installer | \
sh -s -- install
# If flakes are not enabled, enable them with:
mkdir -p ~/.config/nix &&echo"experimental-features = nix-command flakes">>~/.config/nix/nix.conf# Automatic activation with direnv (recommended)
direnv allow
# Or manual activation
nix developThe Nix development environment includes:
- Python with uv package manager
- Automatic dependency installation
- Git hooks (treefmt + ty) auto-configured
- Consistent environment across all platforms
Apache 2.0 License