Skill v1.0.2
currentAutomated scan100/1001 files
version: "1.0.2"
name: develop_extension description: This is a complete guide for creating a new OpenBB Platform extension from scratch. Follow every phase in order. When the user says "build me an application that does X", use this guide to scaffold, implement, install, and verify the extension.
Build an OpenBB Platform Extension
This is a complete guide for creating a new OpenBB Platform extension from scratch. Follow every phase in order. When the user says "build me an application that does X", use this guide to scaffold, implement, install, and verify the extension.
Phase 1 — Scaffold the Project
The openbb-cookiecutter package must be installed in the active Python environment before scaffolding. Install it with:
pip install openbb-cookiecutter
Then run the CLI to generate the project skeleton. All variables have sensible defaults; override only what you need.
Template Variables
| Variable | Default | Description | |
|---|---|---|---|
full_name | "Hello World" | Author name | |
email | "hello@world.com" | Author email | |
project_name | "OpenBB Python Extension Template" | Human-readable project name | |
project_tag | derived from project_name | Hyphenated slug (used as directory name and package identifier) | |
package_name | derived from project_name | Python package name (lower_snake_case) | |
provider_name | derived from project_name | Provider identifier (lower_snake_case) | |
router_name | derived from project_name | Router identifier (lower_snake_case) | |
obbject_name | derived from project_name | OBBject accessor name (lower_snake_case) |
All derived values are automatically generated from project_name — you only need to supply project_name for most cases.
CLI Command
openbb-cookiecutter \-o /path/to/output \--no-input \--extra-context project_name="My Extension Name"
Add more --extra-context KEY=VALUE pairs to override individual variables. Use -f to overwrite an existing directory.
Phase 2 — Understand the Generated Structure
After scaffolding, you get this tree (with template variables resolved):
<project_tag>/├── pyproject.toml # Dependencies, entry points (CRITICAL)├── <package_name>/│ ├── providers/│ │ └── <provider_name>/│ │ ├── __init__.py # Provider registration (fetcher_dict)│ │ ├── models/│ │ │ ├── example.py # Custom-schema fetcher example│ │ │ └── ohlc_example.py # Standard-model fetcher example│ │ └── utils/│ │ └── helpers.py # Shared utility functions│ ├── routers/│ │ ├── __init__.py│ │ ├── <router_name>.py # Router commands (API endpoints)│ │ ├── <router_name>_views.py # Chart views (optional)│ │ └── depends.py # Dependency injection│ └── obbject/│ └── <obbject_name>/│ └── __init__.py # OBBject accessors (optional)└── tests/└── conftest.py
The Four Plugin Types
OpenBB discovers extensions via Python entry points in pyproject.toml. Each plugin type has its own entry point group:
| Entry Point Group | What It Registers | Where It Lives | |
|---|---|---|---|
openbb_provider_extension | Data provider (fetchers) | providers/<name>/__init__.py | |
openbb_core_extension | Router (API endpoints/commands) | routers/<name>.py | |
openbb_charting_extension | Chart views | routers/<name>_views.py | |
openbb_obbject_extension | Result post-processing accessors | obbject/<name>/__init__.py |
Phase 3 — Implement the Data Provider
This is the core of any extension. A provider fetches data from an external source and returns it as typed Pydantic models.
The Three-Class Fetcher Pattern
Every data fetcher follows this structure:
Class 1 — QueryParams (input schema):
- Inherits from
openbb_core.provider.abstract.query_params.QueryParams - Define all parameters the user can pass (e.g.,
symbol,start_date) - Use
pydantic.Fieldfor descriptions and defaults
Class 2 — Data (output schema):
- Inherits from
openbb_core.provider.abstract.data.Data - Define all fields in the response (e.g.,
open,high,low,close,volume) - Use
pydantic.Fieldfor descriptions - Use
__alias_dict__to map source field names to your schema names
Class 3 — Fetcher (orchestrator):
- Inherits from
Fetcher[YourQueryParams, list[YourData]] - Has exactly three static methods:
transform_query(params: dict) -> YourQueryParams
Pre-process user input. Return a validated QueryParams instance.
extract_data(query, credentials, **kwargs) -> list[dict]
Make the actual HTTP request to the data source. Return raw data as dicts. For async fetching, name it aextract_data instead.
transform_data(query, data, **kwargs) -> list[YourData]
Convert raw dicts into typed Data model instances.
Imports for Fetcher Files
from typing import Anyfrom openbb_core.provider.abstract.data import Datafrom openbb_core.provider.abstract.fetcher import Fetcherfrom openbb_core.provider.abstract.query_params import QueryParamsfrom pydantic import Field
HTTP Requests — Required Utilities
IMPORTANT: All HTTP requests inside fetchers and utility helpers must use the built-in utilities from openbb_core.provider.utils.helpers. Do not create raw requests, aiohttp, or httpx clients from scratch. The built-in helpers apply the user's configured HTTP settings (proxy, timeout, user-agent, etc.) from system_settings.json automatically.
Building Query Strings
Convert a QueryParams model to a URL query string, optionally excluding parameters that should not appear in the URL:
from openbb_core.provider.utils.helpers import get_querystringquery_string = get_querystring(query.model_dump(), ["interval", "provider"])url = f"https://api.example.com/data?{query_string}"
model_dump() strips None values automatically. Pass parameter names to exclude in the second argument as a list (use [] if nothing to exclude).
Synchronous Requests
For use inside extract_data (sync fetchers):
from openbb_core.provider.utils import make_request# Returns a requests.Response objectresponse = make_request(url, headers={"Authorization": f"Bearer {api_key}"})data = response.json()
All requests.get/requests.post keyword arguments are passed through.
If you need a session object for multiple requests:
from openbb_core.provider.utils.helpers import get_requests_sessionsession = get_requests_session()response = session.get(url)
Asynchronous Requests (Preferred)
For use inside aextract_data (async fetchers). Always prefer async.
Single URL — returns parsed JSON by default:
from openbb_core.provider.utils.helpers import amake_requestdata = await amake_request(url) # returns dict (parsed JSON)
Multiple URLs — downloads concurrently and returns a list:
from openbb_core.provider.utils.helpers import amake_requestsurls = [f"https://api.example.com/data/{s}" for s in symbols]all_data = await amake_requests(urls) # returns list[dict]
Custom response handling (e.g., CSV): Both amake_request and amake_requests default to parsing JSON. For non-JSON content (CSV, text, binary), pass a response_callback:
from io import StringIOfrom typing import Anyfrom pandas import read_csvfrom openbb_core.provider.utils.helpers import amake_requestresults: list[dict] = []async def csv_callback(response, _: Any):"""Parse CSV response into list of dicts."""text = await response.text()df = read_csv(StringIO(text))results.extend(df.to_dict("records"))await amake_request(url, response_callback=csv_callback)# results now contains the parsed rows
For CSV files with header rows to skip, pass skiprows to read_csv:
async def csv_callback(response, _: Any):text = await response.text()df = read_csv(StringIO(text), skiprows=3)results.extend(df.to_dict("records"))
Async session object — if you need a raw aiohttp.ClientSession:
from openbb_core.provider.utils.helpers import get_async_requests_sessionasync with await get_async_requests_session() as session:async with session.get(url) as response:if response.status != 200:raise OpenBBError(f"Failed: {response.status} -> {response.reason}")data = await response.json()
Summary: Which Helper to Use
| Scenario | Function | Module | |
|---|---|---|---|
| Build a query string | get_querystring() | openbb_core.provider.utils.helpers | |
| Sync single request | make_request() | openbb_core.provider.utils | |
| Sync session | get_requests_session() | openbb_core.provider.utils.helpers | |
| Async single request (JSON) | amake_request() | openbb_core.provider.utils.helpers | |
| Async multiple URLs (JSON) | amake_requests() | openbb_core.provider.utils.helpers | |
| Async single/multi (CSV/text) | amake_request() + response_callback | openbb_core.provider.utils.helpers | |
| Async raw session | get_async_requests_session() | openbb_core.provider.utils.helpers |
Using Standard Models (Multi-Provider Endpoints)
To plug into existing endpoints that other providers already serve (like EquityHistorical), inherit from the standard query/data classes instead of the abstract base classes:
from openbb_core.provider.standard_models.equity_historical import (EquityHistoricalData,EquityHistoricalQueryParams,)
Then add provider-specific fields as extra attributes. Use __alias_dict__ to map the source's field names to the standard field names.
Registering Fetchers in the Provider
In providers/<provider_name>/__init__.py, create a Provider instance:
from openbb_core.provider.abstract.provider import Providermy_provider = Provider(name="my_provider",description="Description of what this provider does.",# credentials=["api_key"], # Uncomment if an API key is neededwebsite="https://example.com",fetcher_dict={"MyCustomModel": MyCustomFetcher,"EquityHistorical": MyEquityHistoricalFetcher, # Plugs into existing endpoint},)
The fetcher_dict keys are model names. When a key matches a standard model name (like "EquityHistorical"), this provider becomes selectable via the provider parameter on that existing endpoint.
For custom/new model names, you must also create a router command that references that model name (see Phase 4).
Credentials
If your provider needs an API key:
- Add
credentials=["api_key"]to the Provider constructor - Access it in
extract_dataviacredentials.get("<package_name>_api_key") - Users configure it in their OpenBB user settings
Phase 4 — Implement the Router
The router defines the API endpoints (commands) that users call.
Router Basics
from openbb_core.app.router import Routerrouter = Router(prefix="")
The top-level prefix is determined by the entry point name in pyproject.toml, not by the prefix argument. Only set prefix for sub-routers.
Provider-Backed Command (Standard Pattern)
This is the most common pattern. It connects 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,StandardParams,)from openbb_core.app.query import Queryfrom pydantic import BaseModel@router.command(model="MyCustomModel")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 (cc, provider_choices, standard_params, extra_params) and the OBBject.from_query(Query(**locals())) return pattern are mandatory and must be used exactly as shown.
The model="MyCustomModel" string must match a key in at least one provider's fetcher_dict.
Free-Form GET Endpoint
For endpoints that don't use the provider/fetcher system:
@router.command(methods=["GET"])async def my_endpoint(symbol: str = "AAPL") -> OBBject[dict]:"""Get some data directly."""# Make HTTP requests, compute, etc.return OBBject(results={"key": "value"})
Free-Form POST Endpoint
@router.command(methods=["POST"])async def my_post_endpoint(data: BaseModel, # Body parametersflag: bool = False, # Query parameters) -> OBBject[dict]:"""Process submitted data."""return OBBject(results={"processed": True})
Dependency Injection
Use routers/depends.py for shared dependencies:
from typing import Annotatedimport requestsfrom fastapi import Dependsfrom openbb_core.provider.utils.helpers import get_requests_sessionSession = Annotated[requests.Session, Depends(get_requests_session)]
Then use session: Session as a parameter in your router commands.
Adding Examples
from openbb_core.app.model.example import APIEx, PythonEx@router.command(model="MyModel",examples=[PythonEx(description="Get data for AAPL",code=["obb.my_router.my_command(symbol='AAPL')"],)],)
Phase 5 — Entry Points in pyproject.toml
This is critical — OpenBB discovers your code entirely through these entry points.
Provider Entry Point
[tool.poetry.plugins."openbb_provider_extension"]my_provider = "my_package.providers.my_provider:my_provider_variable"
The variable (my_provider_variable) is the Provider(...) instance you created in Phase 3.
Router Entry Point
[tool.poetry.plugins."openbb_core_extension"]my_router = "my_package.routers.my_router:router"
The entry point name (my_router) determines the API path prefix. For example, my_router means endpoints appear under /my_router/....
Charting Entry Point (Optional)
[tool.poetry.plugins."openbb_charting_extension"]my_router = "my_package.routers.my_router_views:MyRouterViews"
OBBject Entry Point (Optional)
[tool.poetry.plugins."openbb_obbject_extension"]my_accessor = "my_package.obbject.my_obbject:ext"my_namespace = "my_package.obbject.my_obbject:class_ext"
Phase 6 — Chart Views (Optional)
If you want to add charting support:
from typing import TYPE_CHECKING, Anyif TYPE_CHECKING:from openbb_charting.core.openbb_figure import OpenBBFigureclass MyRouterViews:"""Chart views for the router."""@staticmethoddef my_router_my_command(**kwargs) -> tuple["OpenBBFigure", dict[str, Any]]:"""Create a chart for my_command results."""from openbb_charting.core.openbb_figure import OpenBBFiguredata = kwargs["obbject_item"]fig = OpenBBFigure()# Build your chart using 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.
Phase 7 — OBBject Accessors (Optional)
Result post-processing extensions that add methods to the OBBject response.
Function Accessor (Property-Like)
from openbb_core.app.model.extension import Extensionext = Extension(name="to_csv", description="Convert results to CSV string.")@ext.obbject_accessordef to_csv(obbject, **kwargs) -> str:"""Convert to CSV."""return obbject.to_dataframe().to_csv()
Class Accessor (Namespaced Methods)
class_ext = Extension(name="my_tools", description="Custom result tools.")@class_ext.obbject_accessorclass MyTools:def __init__(self, obbject):self._obbject = obbjectdef summary(self, **kwargs):"""Return a summary."""df = self._obbject.to_dataframe()return df.describe()
Phase 8 — Install, Build, and Test
Install in Development Mode
From the generated project root directory:
pip install -e ".[dev]"
This registers the entry points so OpenBB discovers your extension immediately.
Build Static Assets with openbb-build
CRITICAL: After installing a new extension, or after making changes to any of the following, you must run openbb-build before using the Python interface (obb.<router>.<command>(...)):
- Model definitions —
QueryParamsorDataclasses (field names, types,
defaults, descriptions)
- Provider registration — changes to
fetcher_dictkeys, adding/removing
fetchers
- Router commands — adding, removing, or renaming
@router.command()
endpoints
- Entry points — changes to
pyproject.tomlplugin entries - Any importable item in the registration chain — the
Provider(...)
instance, router module, or model module paths
You do not need to re-run openbb-build when changing:
- Logic inside
extract_data/aextract_data/transform_data/
transform_query static methods (the Fetcher method bodies)
- Utility/helper functions
- Internal implementation details that don't affect the public schema
openbb-build
This regenerates the static assets (type stubs, package interface, provider maps) that the Python interface relies on. Without this step, new or modified commands will not appear on the obb object and calls will fail.
When running as an API server (e.g., via uvicorn or the MCP server), static assets are not used — the API discovers extensions dynamically at startup. You do not need to run openbb-build for API-only usage.
Verify Installation
Start a Python session and check:
from openbb import obb# Your new commands should appear:# obb.<router_name>.<command_name>(...)
Or start the API server and verify the new endpoints appear.
Run Tests
pytest tests/ -v
The generated tests/conftest.py sets OPENBB_AUTO_BUILD=true for proper test environment setup.
Workflow Summary
When a user asks "Build me an application that does X":
- Analyze — Determine what data sources are needed, what endpoints to expose,
and whether to use standard models or custom schemas.
- Scaffold — Run
openbb-cookiecutterwith a meaningfulproject_name. - Delete examples — Remove
example.pyandohlc_example.pyfrom the models
directory. Clean up the example router commands.
- Implement models — Create
QueryParams+Data+Fetcherclasses for each
data source in providers/<name>/models/.
- Register fetchers — Update
providers/<name>/__init__.pywith thefetcher_dict. - Implement router — Create
@router.command(model="...")endpoints in
routers/<name>.py.
- Update entry points — Ensure
pyproject.tomlentry points match your actual
module paths and variable names.
- Add dependencies — Add any third-party packages to
[tool.poetry.dependencies]
in pyproject.toml.
- Install — Run
pip install -e ".[dev]"from the project root. - Build — Run
openbb-buildto regenerate static assets for the Python
interface. Skip this step if only using the API server.
- Test — Verify the commands work, then write tests.