Custom DataSources
Pull data from SaaS APIs, proprietary databases, or any custom source. Data flows into DuckDB just like built-in sources.
Plugins let you extend bino with capabilities that go beyond what YAML manifests and SQL queries can express. A single plugin binary can provide any combination of:
Custom DataSources
Pull data from SaaS APIs, proprietary databases, or any custom source. Data flows into DuckDB just like built-in sources.
Visual Components
Ship Web Component JS/CSS and generate HTML for custom chart types, widgets, or layout elements.
Lint Rules
Validate manifests with domain-specific rules.
Pipeline Hooks
React to pipeline events (after datasets run, after HTML is generated) to transform data, inject content, or collect telemetry.
Plugins are standalone executables that communicate with the bino CLI over gRPC.
bino launches each plugin as a subprocess, sends an Init call, and then invokes
RPCs as the pipeline progresses. When the command finishes, plugins are shut down
gracefully.
This architecture means plugins can be written in any language that supports gRPC, though the official Plugin SDK is for Go.
Build the plugin binary
Place it where bino can find it
bino searches for plugins in this order:
| Priority | Location | Example |
|---|---|---|
| 1 | Explicit path in bino.toml |
path = "./bin/bino-plugin-myplugin" |
| 2 | Project-local | <project>/.bino/plugins/bino-plugin-myplugin |
| 3 | User-global | ~/.bino/plugins/bino-plugin-myplugin |
| 4 | System PATH | Any directory in $PATH |
The binary must be named bino-plugin-<name> where <name> matches the
key in bino.toml.
Declare it in bino.toml
Verify
A plugin declared in bino.toml must load. If it does not (the binary is
missing, its Init returns an error, or it does not match the version pin),
bino build, preview, serve, lint, graph and schema stop with exit
code 1:
Editors keep working without the plugin and show this error on bino.toml
(code plugin-load-failed). [lint] disable does not hide it. Plugins load
once when the editor process starts. In VS Code the daemon and the language
server both hold them, so after you fix the problem reload the window
(Developer: Reload Window). Bino: Restart Daemon is not enough: the
language server keeps talking to the old daemon.
To build without a plugin, remove its [plugins.<name>] table.
Since plugins are native executables, you need a separate binary per platform. The naming convention is:
| Platform | Binary name |
|---|---|
| macOS / Linux | bino-plugin-myplugin |
| Windows | bino-plugin-myplugin.exe |
On macOS, you may need to sign the binary before it can run:
Plugins receive their configuration from bino.toml at startup:
The config map is passed to the plugin’s Init RPC as key-value pairs. The
values are passed as written: bino does not expand ${VAR} in them. With the
Go SDK (v0.0.4 and later), the plugin reads them in its OnInit callback.
To keep a secret out of bino.toml, let the plugin read it from its
environment (for example MY_API_KEY). The plugin process inherits bino’s
environment. Values from a [build.env], [preview.env] or
[serve.env] table in bino.toml are part of that environment only in
bino build, bino preview and bino serve, and in the editor daemon, which
uses [preview.env].
version is a semver constraint, either an exact version ("1.2.0") or a
range (">=1.0 <2.0", "^1.2"). bino checks it against the version the plugin
reports, and a plugin that does not match fails to load.
A plugin can register new kind values that behave like built-in DataSources.
When bino encounters a document with a plugin-provided kind, it calls the
plugin’s CollectDataSource RPC to fetch data.
Plugins can return data as JSON rows (registered as a DuckDB view) or as a
DuckDB SQL expression (e.g., read_parquet('s3://...')).
Plugins can ship Web Components and render custom HTML for visual kinds:
The plugin generates HTML (e.g., <example-chart ...>) and provides the
JS/CSS assets that implement the Web Component. The HTML and the scripts follow
the front-end contract.
Each asset needs a URL path under /plugins/<plugin-name>/, for example
/plugins/example/chart.js. bino serves these paths in bino preview,
bino serve, and to the browser that renders PDFs and screenshots. Absolute
https:// URLs are linked as they are. Any other path logs a warning and is
not served. A path under another plugin’s /plugins/ prefix is skipped, so
one plugin cannot replace another plugin’s files.
bino writes the script and style tags in the order the plugin returns its
assets, plugin by plugin in load order, so a library listed before the code
that uses it also loads first. If two assets have the same path, the first one
is kept and bino logs a warning. A style whose media type is a font, an image,
or any other type that is not CSS is served but gets no <link> tag. A style
without a media type is linked unless its file extension marks it as a font or
an image, so an external stylesheet URL without a .css suffix still works.
A component kind can be used as a child of a LayoutPage or LayoutCard, as
a Grid cell, or as a Tree node, inline or by ref to a document of that
kind:
The spec is validated against the plugin’s schema when the project loads, for
inline children and for top-level plugin documents. A spec.dataset name links
the component to that dataset in the dependency graph. The name the plugin
receives is the ref if set, else metadata.name, else empty. Tree nodes have
no metadata, so they send the ref or nothing.
Warning and error diagnostics that a plugin returns from RenderComponent
appear as warnings in the bino build output and in the build log, prefixed
with the plugin, the kind and the name. In bino preview and bino serve they
appear only in the terminal log. Info diagnostics are logged at debug level.
Diagnostics never fail the render: to fail it, return an error.
A DocumentArtefact can embed a top-level plugin document with
:ref[Kind:name], for example :ref[ExampleChart:revenue_chart]. The plugin
receives the document’s metadata.name and spec, and the plugin assets are
added to the document <head>. In a document, warning and error
diagnostics and a failed render appear in the terminal log. A failed render
shows a placeholder in place of the component and does not fail the document.
The HTML a component returns and the scripts that draw it follow these rules:
<highcharts-chart>. The bn- prefix belongs
to the template engine and bino- to the preview UI. bino warns once per
kind when the root tag uses one of them.GetAssets. Do not put an executable inline
<script> in the HTML: it runs in bino build but not in bino preview,
which merges the new page into the old one. A
<script type="application/json"> data block is fine. bino warns once per
kind about any other <script>.observedAttributes and redraw in attributeChangedCallback:
on a reload, bino preview keeps the element and only updates its
attributes.window.binoPlugin?.track(this, promise), in connectedCallback and on
every redraw. window.binoPlugin exists only while bino prints a PDF or
takes screenshots. In bino preview, bino serve and older bino releases
the optional chaining makes the call a no-op.GetAssets returns them, with the normal HTML
rules: classic scripts run before module scripts. List a library before the
element that uses it.When a loaded plugin provides a component kind, report PDFs, screenshots and
document PDFs wait until every tracked promise has settled and the page has
been quiet for a short moment. bino build then fails when a tracked promise
rejected, when a script under /plugins/ threw an error, or when a custom
element in the page was never defined because its script did not load or did
not call customElements.define. The PDF button in bino preview shows these
as warnings and still returns the PDF.
Besides the kind, the name, the spec and the render mode, the render request carries this (Go SDK v0.0.5 and later):
RenderRequest.Datasets holds the rows of the DataSets named in
spec.dataset, in spec order, each name once. A name that is missing from
it is fetched with GetDatasetResult. A name is missing when the DataSet is
too large for one request (about 3.5 MiB together with the spec), when the
component is a :ref embed in a DocumentArtefact, or when bino is older.
$ DataSource names are never in it, and GetDatasetResult does not serve
them either: DataSource rows are not available to plugins.RenderRequest.InheritedStyle is the name of the ComponentStyle inherited
from the nearest artefact, page, card, grid or tree, or empty. bino does not
write it into the spec. The plugin decides if a style field in its own spec
wins, and reads the style with GetDocument (kind ComponentStyle) or from
the <bn-component-style> elements in the page.Other context comes from the spec or the page:
LayoutPage params are already expanded in the spec.<html lang> or the nearest bn-context[locale].Limits:
_spec_ dataset defaults do not apply to plugin kinds. The rows still carry
the _spec_ columns.Plugin lint rules run alongside bino’s built-in rules during bino lint,
bino build, and bino preview (with --lint):
Plugins subscribe to pipeline checkpoints:
post-dataset-execute - after SQL queries runpost-render-html - after HTML is generatedThese are the only checkpoints bino dispatches. A plugin that subscribes to any other checkpoint gets a warning when it loads.
Hooks can inspect and modify the pipeline payload (e.g., transform HTML, validate datasets, collect metrics).
Plugins can add subcommands to bino:
Plugin callbacks can query the host’s state:
QueryDuckDB runs each query in a throwaway DuckDB session. The session holds
one view per executed DataSet of the artefact being rendered, named by its
metadata.name. DataSources are not visible. DataSets are available from the
post-dataset-execute hook onward, so in RenderComponent and in later hooks.
Before that hook, for example in CollectDataSource, bino build shows no
DataSets, but bino preview and bino serve can still show the DataSets of
the previous render. In bino preview, an embed or presentation render that
runs at the same time can also replace the DataSets at any point, so a query
can see another artefact’s DataSets. QueryDuckDB is not available in
bino lint or the language server. Nothing a query changes is kept: a
DROP VIEW or CREATE TABLE ends with the session. A DataSet that cannot be
loaded as a view is skipped with a warning diagnostic, and the query still runs.
A failing query returns an error diagnostic.
This enables data-aware plugins that make decisions based on the current report’s data and configuration.
See the Plugin SDK documentation for a complete guide to building plugins with Go, including the API reference.
A minimal plugin looks like:
bino preview checks the plugin binaries and bino.toml once a second. When
one changes, for example after go build, preview restarts all plugins and
reloads the page, so the new scripts and output show up without a restart. If
the new build does not load, preview keeps running and shows the error until
the next change fixes it. Two cases still need a restart: adding the first
plugin to a project that had none, and the editor (bino lsp and the daemon),
which loads plugins once.
version pin only checks the binary you installed.