Develops custom FiftyOne plugins (operators and panels) from scratch. Use when creating plugins, extending FiftyOne with custom operators, building interactive panels, or integrating external APIs.
ALWAYS follow these rules:
Ask clarifying questions. Never assume what the plugin should do.
Present file structure and design. Get user approval before generating code.
# Clone official plugins for reference
git clone https://github.com/voxel51/fiftyone-plugins.git /tmp/fiftyone-plugins 2>/dev/null || true
# Search for similar patterns
grep -r "keyword" /tmp/fiftyone-plugins/plugins/ --include="*.py" -l
list_plugins(enabled=True)
list_operators(builtin_only=False)
get_operator_schema(operator_uri="@voxel51/brain/compute_similarity")
# Get plugins directory
PLUGINS_DIR=$(python -c "import fiftyone as fo; print(fo.config.plugins_dir)")
# Develop plugin in plugins directory
cd $PLUGINS_DIR/my-plugin
Write tests:
pytest for operators/panelsvitest for React componentsVerify in FiftyOne App before done.
Run server separately to see logs:
# Terminal 1: Python logs
python -m fiftyone.server.main
# Terminal 2: Browser at localhost:5151 (JS logs in DevTools console)
fiftyone app debug # logs printed to shell; use with a dataset:
fiftyone app debug <dataset-name>
For automated iteration, use Playwright e2e tests:
npx playwright test
Refine until the plugin works as expected.
# Chain operators (non-delegated operators only, in execute() only, fire-and-forget)
ctx.trigger("@plugin/other_operator", params={...})
# UI operations
ctx.ops.notify("Done!")
ctx.ops.set_progress(0.5)
# Use ctx.target_view() to respect user's current selection and filters
view = ctx.target_view()
# ctx.dataset - Full dataset (use when explicitly exporting all)
# ctx.view - Filtered view (use for read-only operations)
# ctx.target_view() - Filtered + selected samples (use for exports/processing)
# Use namespaced keys to avoid cross-dataset conflicts
def _get_store_key(self, ctx):
plugin_name = self.config.name.split("/")[-1]
return f"{plugin_name}_store_{ctx.dataset._doc.id}_{self.version}"
store = ctx.store(self._get_store_key(ctx))
# ctx.panel.state - Transient (resets when panel reloads)
# ctx.store() - Persistent (survives across sessions)
def on_load(self, ctx):
ctx.panel.state.selected_tab = "overview" # Transient
store = ctx.store(self._get_store_key(ctx))
ctx.panel.state.config = store.get("user_config") or {} # Persistent
Use for operations that: process >100 samples or take >1 second.
@property
def config(self):
return foo.OperatorConfig(
name="heavy_operator",
allow_delegated_execution=True,
default_choice_to_delegated=True,
)
@property
def config(self):
return foo.OperatorConfig(
name="progress_operator",
execute_as_generator=True,
)
def execute(self, ctx):
total = len(ctx.target_view())
for i, sample in enumerate(ctx.target_view()):
# Process sample...
yield ctx.trigger("set_progress", {"progress": (i+1)/total})
yield {"status": "complete"}
Use Custom Runs for operations needing reproducibility and history tracking:
# Create run key (must be valid Python identifier - use underscores, not slashes)
run_key = f"my_plugin_{self.config.name}_v1_{timestamp}"
# Initialize and register
run_config = ctx.dataset.init_run(operator=self.config.name, params=ctx.params)
ctx.dataset.register_run(run_key, run_config)
See PYTHON-OPERATOR.md for full Custom Runs pattern. See EXECUTION-STORE.md for advanced caching patterns. See HYBRID-PLUGINS.md for Python + JavaScript communication.
Understand what the user needs to accomplish:
composite_view=True — it produces "Unsupported View" errors for modal panels.@org/plugin-name)See PLUGIN-STRUCTURE.md for file formats.
Create these files:
| File | Required | Purpose |
|---|---|---|
fiftyone.yml |
Yes | Plugin manifest |
__init__.py |
Yes | Python operators/panels |
requirements.txt |
If deps | Python dependencies |
package.json |
JS only | Node.js metadata |
src/index.tsx |
JS only | React components |
Reference docs:
For JavaScript panels with rich UI: Invoke the fiftyone-voodo-design skill for VOODO components (buttons, inputs, toasts, design tokens). VOODO is FiftyOne's official React component library.
list_plugins(enabled=True) # Should show your plugin
list_operators() # Should show your operators
If not found: Check fiftyone.yml syntax, Python syntax errors, restart App.
get_operator_schema(operator_uri="@myorg/my-operator")
Verify inputs/outputs match your expectations.
set_context(dataset_name="test-dataset")
launch_app()
execute_operator(operator_uri="@myorg/my-operator", params={...})
Common failures:
| Type | Language | Use Case |
|---|---|---|
| Operator | Python | Data processing, computations |
| Panel | Hybrid (default) | Python backend + React frontend (recommended) |
| Panel | Python-only | Simple UI without rich interactivity |
| Option | Default | Effect |
|---|---|---|
dynamic |
False | Recalculate inputs on change |
execute_as_generator |
False | Stream progress with yield |
allow_immediate_execution |
True | Execute in foreground |
allow_delegated_execution |
False | Background execution |
default_choice_to_delegated |
False | Default to background |
unlisted |
False | Hide from operator browser |
on_startup |
False | Execute when app starts |
on_dataset_open |
False | Execute when dataset opens |
| Option | Default | Effect |
|---|---|---|
allow_multiple |
False | Allow multiple panel instances |
surfaces |
"grid" | Where panel can display ("grid", "modal", "grid modal") |
category |
None | Panel category in browser |
priority |
None | Sort order in UI |
| Type | Method |
|---|---|
| Text | inputs.str() |
| Number | inputs.int() / inputs.float() |
| Boolean | inputs.bool() |
| Dropdown | inputs.enum() |
| File | inputs.file() |
| View | inputs.view_target() |
fiftyone.yml:
name: "@myorg/hello-world"
type: plugin
operators:
- hello_world
init.py:
import fiftyone.operators as foo
import fiftyone.operators.types as types
class HelloWorld(foo.Operator):
@property
def config(self):
return foo.OperatorConfig(
name="hello_world",
label="Hello World"
)
def resolve_input(self, ctx):
inputs = types.Object()
inputs.str("message", label="Message", default="Hello!")
return types.Property(inputs)
def execute(self, ctx):
print(ctx.params["message"])
return {"status": "done"}
def register(p):
p.register(HelloWorld)
| Log Type | Location |
|---|---|
| Python backend | Terminal running the server |
| JavaScript frontend | Browser console (F12 → Console) |
| Network requests | Browser DevTools (F12 → Network) |
| Operator errors | Operator browser in FiftyOne App |
fiftyone app debug # server logs printed to shell
fiftyone app debug <dataset-name> # with a dataset pre-loaded
def execute(self, ctx):
# Use print() for quick debugging (shows in server terminal)
print(f"Params received: {ctx.params}")
print(f"View stages: {ctx.view.stages}")
print(f"View pipeline: {ctx.view._pipeline()}")
# For structured logging
import logging
logging.debug(f"View stages: {ctx.target_view().stages}")
logging.debug(f"View pipeline: {ctx.target_view()._pipeline()}")
# ... rest of execution
// Use console.log in React components
console.log("Component state:", state);
console.log("Panel data:", panelData);
// Check browser DevTools:
// - Console: JS errors, syntax errors, plugin load failures
// - Network: API calls, variable values before/after execution
Plugin not appearing:
fiftyone.yml exists in plugin root~/.fiftyone/plugins/Operator not found:
fiftyone.ymlregister() functionlist_operators() to debugSecrets not available:
fiftyone.yml under secrets:# For executing operators outside of FiftyOne App context
import fiftyone.operators as foo
result = foo.execute_operator(operator_uri, ctx, **params)