API reference
The OpenBI SDK’s contract, type by type, generated from the source of OpenBI Studio 0.3.74’s SDK and its documentation comments. The SDK’s zip has the same documentation as OpenBI.Studio.Sdk.xml, for your editor.
Start with IStudioPlugin, what you write, and IStudioHost, what the studio gives. What a plug-in can do explains them by task.
The contract of OpenBI Studio 0.3.78: version 1.2 (SdkInfo.Contract), 12 public types in the OpenBI.Studio.Sdk namespace.
IStudioPlugin
public interface IStudioPlugin
A plug-in of OpenBI Studio: a specialty module (Customer Service ...). The studio makes it (its entry type has a constructor without parameters) when it is turned on, gives it the host, and shows its views as tabs under Specialty; its agent tools join the studio's own, for the agent built into the studio and for agents that connect over MCP.
void Initialize(IStudioHost host)- Once, when the plug-in is turned on (on the UI thread).
IReadOnlyList<PluginView> Views- Its tabs, under Specialty in the studio's navigation.
IReadOnlyList<Type> AgentTools- Its agent tools: [McpServerToolType] classes, made with the studio's services (IStudioHost among them, and what
ConfigureServicesadds). Has a body of its own: a plug-in need not implement it. void ConfigureServices(IServiceCollection services)- Its own services, for its agent tools. Has a body of its own: a plug-in need not implement it.
string? AgentInstructions- What it tells agents about itself (added to the studio's instructions), or null. Has a body of its own: a plug-in need not implement it.
void Shutdown()- When the studio closes or the plug-in is turned off. Has a body of its own: a plug-in need not implement it.
PluginView
public sealed record PluginView(string Id, string Title, string Glyph, string ToolTip, Func<object> Create, bool NeedsProject = false)
A tab of a plug-in: its title and glyph in the navigation, and its view, made once when first shown (on Windows, a WPF FrameworkElement: the SDK itself stays free of WPF, so a plug-in's logic builds and tests anywhere).
Its members: string Id { get; init; }, string Title { get; init; }, string Glyph { get; init; }, string ToolTip { get; init; }, Func<object> Create { get; init; }, bool NeedsProject { get; init; }.
IStudioHost
public interface IStudioHost
What the studio gives its plug-ins.
string StudioVersion- The studio's version (0.3.16).
Task InvokeAsync(Action action)- Runs work on the studio's UI thread (views, dialogs) and waits for it.
void Post(Action action)- Runs work on the UI thread later, without waiting.
object MainWindow- The studio's main window (on Windows, a WPF Window: the owner of a plug-in's dialogs).
Also: Task<T> InvokeAsync<T>(Func<T> func).
The open project and its model
event EventHandler? ProjectChanged- A project was opened or closed, or its model loaded again.
IReadOnlyList<TableInfo> Tables- The model's tables and their columns.
Task<QueryResult> DaxAsync(string dax, CancellationToken ct = default)- An OAX query (compatible with DAX) on the open model (as the developer: all data).
void ShowDax(string dax)- Shows an OAX query in the OAX tab (what the agent ran, to look at).
void ShowView(string view)- Shows one of the studio's views: report, tables, model, dax, prediction, operations-research, quantitative, networks, versions; or asks the person for a project: open-project (a project folder), import-powerbi (a Power BI model, .pbip or TMDL); or new-window: the tab in front (a plug-in's own) in a window of its own, for another screen. A plug-in's own tab keeps its state while another view shows, and in its window.
Also: bool HasProject, string? ProjectName, string? ProjectFolder.
A data plug-in's model (an ERP's or a SaaS's tables)
Task ReloadModelAsync(bool refresh = false, CancellationToken ct = default)- Reads the open project's model again from its folder and loads its data, after the plug-in wrote its files (tables, M queries, measures, relationships: OpenBI.Studio.Core's StudioProject; report pages in its reports folder), as after a git pull. refresh: every table read from its source again; else a table whose definition did not change keeps its rows. A source without a sign-in on this computer asks the developer for one, as the studio's own refresh does. Has a body of its own: a plug-in need not implement it.
Task SetSourceCredentialAsync(string address, string auth, string user, string? secret, CancellationToken ct = default)- Keeps a data source's sign-in in this account's credential store (the engine's, sealed for this user: DPAPI on Windows, the keychain's key on a Mac), as the developer's own sign-in does: the model's queries then reach the source without the secret in the model, its M, the project or the logs. address: a web origin (https://api.example.com) or a source's address; auth: key (user: the header's name, Authorization when empty; secret: its value, Bearer ... for a bearer token), password (user and password) or anonymous. The studio's Data sources change or remove it later. Has a body of its own: a plug-in need not implement it.
Task<bool> HasSourceCredentialAsync(string address, CancellationToken ct = default)- Whether this account's credential store has a sign-in for the address (its secret is never given out). Has a body of its own: a plug-in need not implement it.
Task RemoveSourceCredentialAsync(string address, CancellationToken ct = default)- Removes this account's sign-in for the address, if it has one. Has a body of its own: a plug-in need not implement it.
What plug-ins keep
IPluginStorage Storage(string pluginId)- A plug-in's settings and secrets on this computer (never in a project).
Sign-ins
Task<string?> MicrosoftTokenAsync(string[] scopes, bool interactive, string? clientId = null, string authority = "organizations", string? loginHint = null, CancellationToken ct = default)- A Microsoft access token for the scopes (quietly, else - interactive - Microsoft's sign-in window). clientId: an Entra application of the plug-in's (else the studio's choice for the scopes); authority: organizations, consumers or common.
Privacy
PrivacySettings Privacy- How personal data is protected from agents: off, OpenBI's own detectors, or those and Presidio's (IT's policy can impose it).
Pseudonymizer NewPseudonymizer()- A new pseudonymizer (personal data to numbered tokens and back), with the detectors the settings name.
bool SetPrivacy(string mode, string? presidioUrl, string language = "en")- Sets the studio's privacy (off, identifiers, presidio; Presidio's analyzer address), unless IT's policy imposes it (then false).
The agent
AgentEndpoint Agent- The studio's MCP endpoint, where an agent of one's own connects (Claude Code, Claude Desktop ...): its address, whether it listens, and how an agent connects. Its
Keyis empty (contract 1.2): the studio's agent key is the person's, never a plug-in's. A plug-in that calls the endpoint itself asks withRequestAgentAccessAsync. Task<PluginAgentAccess?> RequestAgentAccessAsync(bool ask = true, CancellationToken ct = default)- This plug-in's own access to the studio's agent endpoint (contract 1.2): a key of its own, for the tools its plugin.json names (
AgentAccess). The person allows it once per version of the plug-in: OpenBI Studio asks them, with the reason plugin.json gives, the first time (ask: false never asks: the access when it is allowed already, else null). Null also when plugin.json asks for none, when the person says no, or when IT's policy turns it off. The key lives while the studio runs and the plug-in is on, in memory only: when the person withdraws it (Specialty modules), or the plug-in is turned off or removed, the endpoint refuses it from the next request andRevokedis cancelled. Every call made with it shows in the Activity panel as the plug-in's. Ask with the host yourInitializewas given (keep it): the host the studio's services give agent tools is the studio's, which knows no plug-in, and answers null. Has a body of its own: a plug-in need not implement it.
The studio
void Log(string title, string detail = "", bool error = false, bool byAgent = false)- A line in the activity panel (byAgent: done for an agent).
void SetStatus(string text)- The status bar's text.
void ShowPluginView(string pluginId, string viewId)- Shows one of the plug-in's tabs.
IPluginStorage
public interface IPluginStorage
A plug-in's settings (a JSON object) and secrets (encrypted for the Windows user), on this computer.
Its members: T Load<T>(string name) where T : new(), void Save<T>(string name, T value), string? Secret(string key), void SetSecret(string key, string? value).
AgentEndpoint
public sealed record AgentEndpoint(string Url, [property: Obsolete("Empty from OpenBI SDK contract 1.2: the studio's agent key is never a plug-in's. Declare agentAccess in plugin.json and call IStudioHost.RequestAgentAccessAsync for a key of the plug-in's own.")] string Key, bool Listening, string ClaudeCommand, string JsonConfig)
The studio's MCP endpoint: its address, whether it listens, and how an agent connects - Claude Code's command and any client's settings, both the stdio server (OpenBI.Studio.exe --mcp) that finds the endpoint and its key by itself: neither has the key in it. Key: empty for plug-ins (contract 1.2; 1.0 and 1.1 gave the studio's agent key).
Its members: string Url { get; init; }, [property: Obsolete("Empty from OpenBI SDK contract 1.2: the studio's agent key is never a plug-in's. Declare agentAccess in plugin.json and call IStudioHost.RequestAgentAccessAsync for a key of the plug-in's own.")] string Key { get; init; }, bool Listening { get; init; }, string ClaudeCommand { get; init; }, string JsonConfig { get; init; }.
PrivacySettings
public sealed record PrivacySettings
Whether, and how, personal data is pseudonymized before an agent sees it.
string Mode- off (personal use) | identifiers (OpenBI's own: identifiers and known names) | presidio (those and Presidio's analyzer).
bool Enforced- IT's policy sets it (the user cannot turn it off).
Also: string? PresidioUrl, string PresidioLanguage, bool On.
PluginManifest
public sealed record PluginManifest
A plug-in's manifest (plugin.json, beside its assembly): who it is, what it needs and where its entry point is.
{ "id": "openbi.customer-service", "name": "Customer Service", "version": "1.0.0", "publisher": "Norway Analytics", "description": "...", "assembly": "OpenBI.CustomerService.dll", "entry": "OpenBI.CustomerService.CustomerServicePlugin", "minStudio": "0.3.16", "sdk": "1.1", "license": { "product": "customer-service", "required": false } }
string Id- In plugin.json:
id. string Name- In plugin.json:
name. string Version- In plugin.json:
version. string Publisher- In plugin.json:
publisher. string Description- In plugin.json:
description. string Assembly- In plugin.json:
assembly. string Entry- In plugin.json:
entry. string? MinStudio- The oldest studio it runs in (its SDK's). In plugin.json:
minStudio. string? Sdk- The SDK contract it was built against (major.minor,
SdkInfo): what its assembly says wins; this, for a plug-in built without the SDK's build targets; neither: 1.0, built before contracts were numbered. In plugin.json:sdk. string? Glyph- Segoe Fluent glyph of its card on the Plug-ins page. In plugin.json:
glyph. PluginLicense? License- Its licence (paid modules): the product a key unlocks, and whether one is needed to run. In plugin.json:
license. PluginAgentAccessRequest? AgentAccess- What it asks of the studio's agent endpoint (contract 1.2): a key of its own, for the tools named, which the person allows once per version (
RequestAgentAccessAsync). Null: it asks for none, and gets none.
In plugin.json:"agentAccess": { "reason": "Starts Claude Code on your project to draft the month's commentary.", "tools": ["studio_status", "model_measures", "dax_query"] }agentAccess.
PluginLicense
public sealed record PluginLicense
string Product- In plugin.json:
product. bool Required- In plugin.json:
required.
SdkInfo
public static class SdkInfo
The contract between OpenBI Studio and its plug-ins, and its version: major.minor. A plug-in built with the SDK carries the contract it was built against in its assembly (the SDK's build targets write [assembly: AssemblyMetadata("OpenBI.Studio.Sdk.Contract", "1.2")]; a plug-in built otherwise can say it in plugin.json: "sdk": "1.2"), and the studio compares it with its own when it finds the plug-in:
- the same major, an older minor: it runs. A minor version only adds (a new member of IStudioHost has a body of its own), so what the plug-in was built against is all still there;
- an older major: it does not run. A major version breaks plug-ins built before it (a member changed or removed): the plug-in must be built again with the new SDK;
- a newer contract than the studio's: it needs a newer OpenBI Studio.
A plug-in that says nothing was built before contracts were numbered (OpenBI SDK 0.3.66 or older): contract 1.0. The contract's version is not the SDK's (the SDK's version is OpenBI Studio's) nor its assemblies' (1.0.0.0, kept so whatever version a build is given). 1.2 also empties AgentEndpoint.Key, which 1.0 and 1.1 filled with the studio's agent key: no plug-in of Norway Analytics' read it (they show the endpoint's address and Claude Code's command, which hold no key), and the key is the person's, for the agents they connect. The member stays (a plug-in that reads it still loads, and gets "").
const string Contract- The contract of this SDK, and of the OpenBI Studio built with it.
const string ContractMetadataKey- The key of the assembly metadata that carries a plug-in's contract.
const string Unstated- The contract of a plug-in that states none: built before contracts were numbered (SDK 0.3.66 or older).
static IReadOnlyList<SdkContractVersion> History- Each version of the contract: the studio it came with, what it added or changed, and - when a plug-in built against an older minor misses something people would notice - a sentence that says so (null: nothing to say).
SdkContractVersion
public sealed record SdkContractVersion(string Contract, string Studio, string Added, string? Note)
A version of the SDK's contract: the studio it came with, what it added, and what a plug-in built before it misses (shown to people only when not null).
Its members: string Contract { get; init; }, string Studio { get; init; }, string Added { get; init; }, string? Note { get; init; }.
PluginAgentAccessRequest
public sealed record PluginAgentAccessRequest
A plug-in's request for the studio's agent endpoint, in its plugin.json (contract 1.2): why, in a sentence the person reads before they allow it, and the agent tools it may call (their names: dax_query, model_measures ...; none named: every tool, the studio's and its plug-ins', as an agent the person connects has).
string Reason- In plugin.json:
reason. IReadOnlyList<string>? Tools- In plugin.json:
tools.
PluginAgentAccess
public sealed record PluginAgentAccess(string Url, string Key, IReadOnlyList<string> Tools, string JsonConfig, CancellationToken Revoked)
A plug-in's own access to the studio's agent endpoint (RequestAgentAccessAsync): the endpoint's address, the plug-in's key (a password: send it as the header Authorization: Bearer <key>, never show it, never write it to a file), the tools it may call (empty: every tool), an MCP client's settings with the key in them (for an agent the plug-in starts itself: pass them in memory or a file only that agent reads, deleted after), and Revoked, cancelled when the key ends (the person withdrew it, the plug-in was turned off or removed).
Its members: string Url { get; init; }, string Key { get; init; }, IReadOnlyList<string> Tools { get; init; }, string JsonConfig { get; init; }, CancellationToken Revoked { get; init; }.
Types from OpenBI.Studio.Core
The contract hands a plug-in these types of the studio's own library (OpenBI.Studio.Core, which the SDK carries as a reference assembly): the open model's tables, a query's rows, the pseudonymizer. Their public members:
TableInfo
public sealed record TableInfo(string Name, bool Hidden, long Rows, IReadOnlyList<ColumnInfo> Columns, IReadOnlyList<string> Measures)
bool Live- A live connection's table: its rows stay on the server.
string? Error- Why a calculated table is empty (a prediction that cannot score yet), or null.
Also: string Name { get; init; }, bool Hidden { get; init; }, long Rows { get; init; }, IReadOnlyList<ColumnInfo> Columns { get; init; }, IReadOnlyList<string> Measures { get; init; }, string? Description, string? DataCategory, bool Calculated, string? Expression, IReadOnlyList<MeasureInfo> MeasureDetails, string RowsText.
ColumnInfo
public sealed record ColumnInfo(string Name, string Type, bool Hidden, bool Calculated, long Cardinality)
bool Override- A live connection's column whose properties this project sets (format, folder, …:
ProjectSets), not the server. string? Problem- A live connection's: what the server's model no longer has for it (renamed or removed there), or null.
string? Warning- What the column's load found that Power BI refuses and OpenBI keeps (a PATH over a parent that is no row's id: the rows, by their ids), or null.
Also: string Name { get; init; }, string Type { get; init; }, bool Hidden { get; init; }, bool Calculated { get; init; }, long Cardinality { get; init; }, string? Format, string? Description, string? Folder, string? DataCategory, string? SortBy, bool IsKey, string? Expression, IReadOnlyList<string> ProjectSets.
MeasureInfo
public sealed record MeasureInfo(string Table, string Name, string Expression, string? Format, string? Folder, string? Description, bool Hidden)
bool Remote- A live connection's measure defined on the server (not one of the project's own).
bool Override- The server's measure whose properties this project sets (format, folder, …:
ProjectSets), not the server. string? Problem- A live connection's: what the server's model no longer has for it (renamed or removed there), or null.
string? Warning- Its name a column's of its table, or another measure's (Power BI refuses both; the model opens): the engine's words, or null.
Also: string Table { get; init; }, string Name { get; init; }, string Expression { get; init; }, string? Format { get; init; }, string? Folder { get; init; }, string? Description { get; init; }, bool Hidden { get; init; }, IReadOnlyList<string> ProjectSets.
QueryResult
public sealed class QueryResult
Rows of a DuckDB preview or a DAX query.
bool IsStatement- A statement (ATTACH, SET, ...) that returns no rows.
int Statements- How many statements a DuckDB text ran (separated by ;): the rows are the last one's.
ServerTimings? Timings- Where a DAX query's time went, when it was asked for (
DaxAsync). QueryPlan? Plan- Its plan as it ran (asked for with plan: true), else null.
IReadOnlyList<DraftScored> Drafts- The model versions no one has approved, or retired ones, that PREDICT("name", n) scored in it (only a studio test call may: asked for with drafts: true): the answer is a test's - a report page or OpenBI Server refuses it. Empty: none.
Also: IReadOnlyList<ResultColumn> Columns, IReadOnlyList<JsonElement[]> Rows, bool Truncated, double Milliseconds, static string Text(JsonElement v).
ResultColumn
public sealed record ResultColumn(string Name, string Type)
A column of a result: its name and its type (DuckDB or DAX).
Its members: string Name { get; init; }, string Type { get; init; }.
Pseudonymizer
public sealed partial class Pseudonymizer
Personal data to numbered tokens and back: what an agent sees is "Hi <PERSON_1>, your card <CARD_1> ...", each person or number one token wherever it is written (the mail, the operator's question, the model's rows), so the agent can still tell them apart and match them; its drafts and tool calls get the real values back here. The tokens and their values stay in this object, in memory on this computer.
NameDetector Names- The names it finds wherever they are written (add the mail's people, the model's person names).
int Count- How many values it has replaced so far.
void Keep(string value)- A value never replaced, wherever it is written: what is not personal data of the people written about (a mailbox's own address: the side the agent writes for).
string TokenFor(string type, string value)- The token of a value (a new one the first time it is seen).
Task<List<PiiSpan>> FindAsync(string text, CancellationToken ct = default)- The spans to replace: the detectors' findings, each on one line, overlaps settled (the longest, then the surest, wins; a guess never runs past the end of a name it knows).
event Action<string>? PresidioFailed- Presidio's analyzer did not answer: the text was pseudonymized by OpenBI's own detectors only. It says why in a fixed short text (PresidioDetector.Reason: "Presidio did not answer (HTTP 503)"), never with the analyzer's answer or the text sent.
Task<string> AnonymizeAsync(string text, CancellationToken ct = default)- The text with its personal data replaced by tokens (tokens already in it are kept).
Task<string> AnonymizeNameAsync(string displayName, CancellationToken ct = default)- A name from a mail's header (From, To, Cc): a person's, whatever the name lists say - learned (so it is found wherever it is written, with their first name and surname alone) and given as a token, never as it is. Its person is one token ("Hansen, Kari" and "Kari Hansen" the same); a company after it ("Ingrid Nordmann - Fjordlast Transport") stays text, itself pseudonymized.
string Restore(string text)- The tokens in a text (an agent's draft, a tool's argument) replaced by their values; unknown ones stay.
JsonNode? Restore(JsonNode? node)- Every string of a JSON value restored (a tool call's arguments).
IReadOnlyList<(string Token, string Value)> Map()- The tokens and their values, for the operator to see what the agent was not shown.
static bool IsToken(string text)- Whether a text is one token, as this gives them (<PERSON_1>).
Also: Pseudonymizer(IEnumerable<IPiiDetector> detectors, NameDetector? names = null).