Skill v1.0.2
currentAutomated scan100/1001 files
version: "1.0.2"
name: build_workspace_app description: This guide covers the full lifecycle of building, running, and serving a custom OpenBB Workspace application from an extension project scaffolded by openbb-cookiecutter. It assumes the project shell already exists (see the develop_extension skill for scaffolding instructions).
Build and Run OpenBB Workspace Applications
This guide covers the full lifecycle of building, running, and serving a custom OpenBB Workspace application from an extension project scaffolded by openbb-cookiecutter. It assumes the project shell already exists (see the develop_extension skill for scaffolding instructions).
Prerequisites
Ensure the following packages are installed in the active Python environment:
pip install openbb-core openbb-platform-api openbb-devtools
openbb-coreprovides the Router, Provider, OBBject, and Fetcher base classes.openbb-platform-apiprovides theopenbb-apiCLI for serving backends
and auto-generating widgets.json for OpenBB Workspace.
openbb-devtoolsprovidespytest, cassette recording, and QA utilities.
If you also need the Python Interface wrapper (the obb object), install the main package:
pip install openbb --no-deps
Architecture Overview
OpenBB is built on FastAPI and Pydantic. The application has two independent interfaces that share core logic and models:
- Python Interface — wraps installed routers into an
obbPython package
with auto-generated docstrings and function signatures. Requires a build step (openbb-build) to generate static assets.
- REST API — a FastAPI instance with all installed routers available via
HTTP. Import it with from openbb_core.api.rest_api import app or launch with uvicorn openbb_core.api.rest_api:app.
The application is the product of all installed extensions. With just openbb-core there are no routers or endpoints — users compose their own combinations.
Key Classes
| Class | Import | Purpose | |
|---|---|---|---|
Router | openbb_core.app.router | Subclass of fastapi.APIRouter; defines commands | |
OBBject | openbb_core.app.model.obbject | Standard response object with results, provider, warnings, extra | |
Provider | openbb_core.provider.abstract.provider | Registers fetchers for the provider interface | |
Fetcher | openbb_core.provider.abstract.fetcher | TET pipeline: Transform query → Extract data → Transform data | |
QueryParams | openbb_core.provider.abstract.query_params | Base class for input parameters | |
Data | openbb_core.provider.abstract.data | Base class for output data schemas |
Router Extensions (API Endpoints)
Router extensions are the user-facing endpoints that power the REST API, MCP, and Python Interface.
Creating a Router
from openbb_core.app.model.obbject import OBBjectfrom openbb_core.app.router import Routerrouter = Router(prefix="", description="My custom extension.")
The top-level API path prefix is determined by the entry point name in pyproject.toml, not the prefix argument. Only use prefix for sub-routers.
[tool.poetry.plugins."openbb_core_extension"]my_app = "my_package.routers.my_router:router"
Commands appear at /my_app/... in the API and obb.my_app. in Python.
Provider Interface Endpoints
Connect a router command to one or more provider fetchers via the model name:
from openbb_core.app.model.command_context import CommandContextfrom openbb_core.app.model.obbject import OBBjectfrom openbb_core.app.provider_interface import ExtraParams, ProviderChoices, StandardParamsfrom openbb_core.app.query import Queryfrom pydantic import BaseModel@router.command(model="MyModel")async def my_command(cc: CommandContext,provider_choices: ProviderChoices,standard_params: StandardParams,extra_params: ExtraParams,) -> OBBject[BaseModel]:"""Description of this command."""return await OBBject.from_query(Query(**locals()))
The four parameters and the OBBject.from_query(Query(**locals())) return are mandatory and must be used exactly as shown.
Basic GET Endpoint
@router.command(methods=["GET"])async def hello() -> OBBject[str]:"""OpenBB Hello World."""return OBBject(results="Hello from the extension!")
Basic POST Endpoint
from openbb_core.provider.abstract.data import Data@router.command(methods=["POST"])async def process(data: Data, some_param: str) -> OBBject:"""Process submitted data."""return OBBject(results=data.model_dump())
Decorator Parameters
@router.command accepts:
model— metamodel name linking to Provider fetchersmethods— list of HTTP methods, typically["GET"]or["POST"]examples— list ofAPIExorPythonExfor docsdeprecated— deprecation noticeexclude_from_api— Python Interface onlyno_validate— skip response validation, treat output asAnyopenapi_extra— dictionary for inlinewidget_configormcp_config
Using FastAPI APIRouter Directly
Access the underlying FastAPI router via router._api_router:
@router._api_router.get("/also_empty")async def also_empty(param: str) -> str:"""Also empty."""return "Hello world!"
From FastAPI — Converting Existing Apps
Any existing FastAPI application can become an OpenBB extension without changing code. Define the entry point in pyproject.toml pointed at the FastAPI or APIRouter instance:
[tool.poetry.plugins."openbb_core_extension"]my_app = "my_package.app:app"
Install the package and build static assets:
pip install -e .openbb-build
Known limitations:
- Authorization hooks are not injected into the Python Interface
- Request-bound dependencies or those returning None are not injected
- WebSockets are not supported in the Python Interface
- Multi-method routes (GET + POST on same path) may not generate correctly
Serving with openbb-api (Workspace Backends)
The openbb-api CLI converts a FastAPI instance into an OpenBB Workspace backend with auto-generated widget definitions.
Basic Usage
# Start with default OpenBB extensionsopenbb-api# Start with a custom FastAPI fileopenbb-api --app ./my_app.py --host 0.0.0.0 --port 8005# Factory function patternopenbb-api --app my_app.py:create_app --factory# Custom FastAPI instance nameopenbb-api --app my_app.py --name my_app
Defaults are --host 127.0.0.1 --port 6900, falling back to the next available port if already in use.
Key Arguments
| Argument | Description | |
|---|---|---|
--app | Path to Python file with a FastAPI instance | |
--name | Name of the FastAPI instance (default: app) | |
--factory | Flag if the app name is a factory function | |
--editable | Make widgets.json editable at runtime | |
--no-build | Load existing widgets.json without checking for updates | |
--exclude | JSON list of API paths to exclude from widgets | |
--widgets-json | Custom path for widgets.json | |
--apps-json | Custom path for workspace_apps.json |
All remaining arguments are passed to uvicorn.run.
Inline Widget Definitions
Widget properties can be defined inline in your code via openapi_extra:
@app.get("/some_endpoint",openapi_extra={"widget_config": {"name": "Custom Widget Name","description": "Override docstring description",}},)async def some_endpoint():"""Description from docstring."""pass
Exclude an Endpoint from Widgets
@app.get("/internal_endpoint",openapi_extra={"widget_config": {"exclude": True}},)async def internal_endpoint():return [{"label": "Choice 1", "value": "choice1"}]
Dropdown Parameters
Dropdowns are auto-generated from Literal types:
from typing import Literal@app.get("/with_dropdown")async def with_dropdown(choices: Literal["Choice 1", "Choice 2", "Choice 3"] = "Choice 3"):pass
Column Definitions for Tables
Use Pydantic response models to auto-generate table column definitions:
import datetimefrom pydantic import BaseModel, Fieldclass MyData(BaseModel):date: datetime.date = Field(description="The date.")value: float = Field(description="The value.")@app.get("/my_data")async def my_data() -> list[MyData]:"""Widget with typed columns."""return [MyData(date=datetime.date.today(), value=42.0)]
Widget Types by Return Type
| Return Type | Widget Type | |
|---|---|---|
list[dict] or list[BaseModel] | Table (AgGrid) | |
str | Markdown | |
dict (with widget_config.type="chart") | Plotly Chart | |
MetricResponseModel | Metric | |
PdfResponseModel |
Response Models for Special Widgets
from openbb_platform_api.response_models import MetricResponseModel, PdfResponseModel@app.get("/metric", response_model=MetricResponseModel)async def metric():"""A metric widget."""return dict(label="Revenue", value=12345, delta=5.67)@app.get("/pdf", response_model=PdfResponseModel)async def open_pdf(file_path: str):"""Open a PDF document."""with open(file_path, "rb") as f:return dict(content=f.read())
Plotly Chart Widget
@app.get("/chart",openapi_extra={"widget_config": {"type": "chart"}},)async def chart() -> dict:"""A chart widget."""from plotly.graph_objs import Bar, Layout, Figurefig = Figure(data=[Bar(x=["A", "B", "C"], y=[1, 2, 3])],layout=Layout(title="My Chart", template="plotly_dark"),)return fig.to_plotly_json()
JSON Schema Extra for Parameters
Annotate parameters with additional widget configuration:
from typing import Annotatedfrom fastapi import Querymy_param: Annotated[str,Query(title="My Title",description="Detailed hover text",json_schema_extra={"x-widget_config": {"optionsEndpoint": "/my_choices_endpoint"}},),]
Form Input Widget
Create an input form tied to a table:
- GET endpoint defines
widget_config.form_endpointpointing to the POST route - POST route accepts a single Pydantic model argument
OBBject Extensions (Result Post-Processing)
Extend the OBBject response with custom methods, accessible in the Python Interface.
Class Accessor (Namespaced Methods)
from openbb_core.app.model.extension import Extensionext = Extension(name="my_tools", description="Custom result tools.")@ext.obbject_accessorclass MyTools:def __init__(self, obbject):self._obbject = obbjectdef summary(self):"""Return a summary."""return self._obbject.to_dataframe().describe()
Register in pyproject.toml:
[tool.poetry.plugins."openbb_obbject_extension"]my_tools = "my_package.obbject.my_ext:ext"
Callable Accessor (Property-Like)
ext = Extension(name="to_csv", description="Convert results to CSV.")@ext.obbject_accessordef to_csv(obbject):"""Convert to CSV string."""return obbject.to_dataframe().to_csv()
OBBject Output Conversion Methods
Every OBBject has built-in conversion methods:
to_df()/to_dataframe()— Pandas DataFrameto_dict(orientation=...)— Python dictto_numpy()— NumPy arrayto_polars()— Polars DataFrame (requirespolarsinstalled)model_dump()— Complete object as dictmodel_dump_json()— Serialized JSON string
OBBject Plugins (Pre-Return Interceptors)
Plugins execute before the response is returned, compatible with both REST API and Python Interface. They can conditionally alter the output of any command.
WARNING: Plugins are considered potentially dangerous. The environment must be explicitly configured to allow them.
In system_settings.json:
{"allow_on_command_output": true,"allow_mutable_extensions": true}
Plugin Configuration
from openbb_core.app.model.extension import Extensionplugin = Extension(name="my_plugin",description="Intercept output before return.",on_command_output=True,command_output_paths=["/my_router/my_command"],immutable=False,results_only=False,)
Key parameters:
on_command_output=True— required for pluginscommand_output_paths— list of endpoint paths to intercept (None = all)immutable=False— set to allow modifying the response objectresults_only=True— receive only theresultsportion instead of full OBBject
Plugin Code
@plugin.obbject_accessordef my_plugin_func(obbject):"""Modify or inspect the response before it returns."""# Modify obbject directly; do NOT return anythingpass
Charting Extensions (Views)
Add custom chart views to any router endpoint, activated when the user sets chart=True.
Structure
from typing import TYPE_CHECKING, Anyif TYPE_CHECKING:from openbb_charting.core.openbb_figure import OpenBBFigureclass MyViews:"""Chart views for the router."""@staticmethoddef my_router_my_command(**kwargs) -> tuple["OpenBBFigure", dict[str, Any]]:"""Chart for my_command."""from openbb_charting.core.openbb_figure import OpenBBFiguredata = kwargs["obbject_item"]fig = OpenBBFigure()# Build chart with fig.add_*() methodscontent = fig.show(external=True).to_plotly_json()return fig, content
Method naming convention: <router_name>_<command_name> matching the route path in lower_snake_case.
Register in pyproject.toml:
[tool.poetry.plugins."openbb_charting_extension"]my_router = "my_package.routers.my_views:MyViews"
Kwargs Available in Views
| Key | Content | |
|---|---|---|
obbject_item | Validated results object | |
charting_settings | User charting preferences | |
standard_params | Standard model parameters | |
extra_params | Provider-specific parameters | |
provider | Provider name used | |
extra | Execution metadata |
HTTP Requests in Fetchers
Use the built-in utilities instead of creating new clients from scratch.
Query String Helper
from openbb_core.provider.utils.helpers import get_querystringquery_string = get_querystring(query.model_dump(), ["exclude_this_param"])
Synchronous Requests
from openbb_core.provider.utils import make_requestresponse = make_request(url, headers=headers, params=params)
Requests Session
from openbb_core.provider.utils.helpers import get_requests_sessionsession = get_requests_session()
Asynchronous Requests (AIOHTTP)
from openbb_core.provider.utils.helpers import amake_requestresponse_json = await amake_request(url)
Multi-URL Async Requests
from openbb_core.provider.utils.helpers import amake_requestsresults = await amake_requests([url1, url2, url3])
Custom Response Callback
from io import StringIOfrom pandas import DataFrameresults = []async def response_callback(response, _):text = await response.text()data = DataFrame(StringIO(text), skiprows=2)results.append(data.to_dict("records"))await amake_requests(url, response_callback=response_callback)
Async Session
from openbb_core.provider.utils.helpers import get_async_requests_sessionasync with await get_async_requests_session() as session:async with await session.get(url) as response:data = await response.json()
Async Fetchers
Use aextract_data instead of extract_data for async fetchers:
@staticmethodasync def aextract_data(query: MyQueryParams,credentials: dict[str, str] | None,**kwargs: Any,) -> list[dict]:"""Async data extraction."""...
Testing
Built-In Fetcher Test
Every Fetcher has a .test() method for quick validation:
from my_package.providers.my_provider.models.my_model import MyFetcherfetcher = MyFetcher()fetcher.test({"symbol": "AAPL"}, {}) # Returns None on success
Unit Tests with Cassettes
Install dev tools and use pytest_recorder for HTTP cassette recording:
pip install openbb-devtoolspytest test_my_fetcher.py --record http
Subsequent test runs replay the recorded HTTP interactions.
Running Tests
# Unit tests onlypytest tests/ -m "not integration"# Integration tests onlypytest tests/ -m integration# All testspytest tests/
Integration Testing
For API integration tests, start a local server first:
uvicorn openbb_core.api.rest_api:app --host 0.0.0.0 --port 8000 --reload
Install and Build
Development Install
From the project root:
pip install -e ".[dev]"
Build Python Interface
After installing or removing extensions, regenerate static assets:
openbb-build
Verify in Python
from openbb import obb# Your commands appear under obb.<router_name>.<command_name>()
Serve as Workspace Backend
openbb-api --app ./my_app.py --editable --host 0.0.0.0 --port 6900
Workflow Summary
When a user asks "Build me a Workspace application that does X":
- Scaffold the project with
openbb-cookiecutter(seedevelop_extensionskill). - Remove example files and clean up boilerplate.
- Implement fetchers for each data source in
providers/<name>/models/. - Register fetchers in
providers/<name>/__init__.pyviafetcher_dict. - Implement router commands in
routers/<name>.py. - Add widget config via
openapi_extrafor Workspace-specific behavior. - Update `pyproject.toml` entry points and dependencies.
- Install with
pip install -e ".[dev]". - Build static assets with
openbb-build. - Serve with
openbb-apiand verify widgets in OpenBB Workspace. - Test with
pytestand fetcher.test()methods.