Entries & Parameters
An entry point is a "function" your plugin exposes to the outside world. Every executable button users see in Plugin Manager, every tool the AI agent can call, every service other plugins can request — those are all entry points.
The simplest entry
You want your plugin to do one thing. Add @plugin_entry to a method:
from plugin.sdk.plugin import NekoPluginBase, neko_plugin, plugin_entry, Ok
@neko_plugin
class MyPlugin(NekoPluginBase):
@plugin_entry(id="hello", name="Say Hello", description="Say hello")
async def hello(self):
return Ok({"message": "Hello!"})This entry has no parameters. Click execute in Plugin Manager, get back {"message": "Hello!"}.
Adding parameters
Most entries need input. Just write parameters in the function signature:
@plugin_entry(id="greet", name="Greet", description="Greet someone by name")
async def greet(self, name: str, times: int = 1):
messages = [f"Hello, {name}!" for _ in range(times)]
return Ok({"messages": messages})The SDK automatically:
- Generates an input form from
name: strandtimes: int = 1(shown in the panel) namehas no default → requiredtimeshas default1→ optional- Uses type annotations as form/schema hints. Direct calls pass plain values through; use a Pydantic model when you need runtime validation.
You don't need to write any JSON Schema.
Adding descriptions to parameters
By default, the panel shows the variable name (name, times). To show friendlier descriptions, use Annotated:
from typing import Annotated
@plugin_entry(id="greet", name="Greet", description="Greet someone by name")
async def greet(
self,
name: Annotated[str, "The person's name"],
times: Annotated[int, "How many times to repeat"] = 1,
):
messages = [f"Hello, {name}!" for _ in range(times)]
return Ok({"messages": messages})Now the panel shows "The person's name" next to name, and "How many times to repeat" next to times.
The AI agent also sees these descriptions, helping it understand what values to pass.
Many parameters: use a Pydantic model
If your entry has many parameters or needs complex validation (min/max, regex, etc.), define them as a Pydantic model:
from pydantic import BaseModel, Field
class SearchParams(BaseModel):
query: str = Field(..., description="Search keywords")
max_results: int = Field(default=10, ge=1, le=50, description="Max results")
language: str = Field(default="zh-CN", description="Result language")
include_images: bool = Field(default=False, description="Include image results")
@plugin_entry(id="search", name="Search", description="Search for content")
async def search(self, params: SearchParams):
self.logger.info("Searching: {} (max {})", params.query, params.max_results)
results = await self._do_search(params.query, params.max_results)
return Ok({"results": results, "count": len(results)})When the SDK sees a function with one parameter typed as a BaseModel, it automatically:
- Generates an input form from the model (with descriptions, defaults, constraint hints)
- Validates input with
model_validate()on call (e.g.max_results=100is rejected becausele=50) - Passes the validated model instance to your function
When to use Pydantic vs plain parameters?
- ≤ 3 parameters, no complex validation → write them in the function signature
- Many parameters, need validation constraints, want better docs → use a Pydantic model
Returning success and failure
Entry points must return Ok(...) or Err(...):
from plugin.sdk.plugin import Ok, Err, SdkError
@plugin_entry(id="divide", name="Divide", description="Divide two numbers")
async def divide(self, a: float, b: float):
if b == 0:
return Err(SdkError("Cannot divide by zero"))
return Ok({"result": a / b})Ok(data)— Success.datacan be a dict, list, string, or number.Err(SdkError("reason"))— Failure. The error message shows in the panel and tells the AI "this call failed".
Why not just raise exceptions? Because plugins run in separate processes — exceptions don't propagate to the main system. Ok/Err is the cross-process safe communication pattern. That said, if your code unexpectedly raises, the framework catches it and converts to Err automatically — no crash.
Controlling what the AI sees
By default, the AI sees all fields you return. But sometimes the return value contains large raw data (like a full search results list) and you only want the AI to see a summary:
@plugin_entry(
id="search",
name="Search",
description="Web search",
llm_result_fields=["summary", "count"],
)
async def search(self, query: str):
results = await self._do_search(query)
summary = self._build_summary(results)
return Ok({
"summary": summary, # ← AI can see this
"count": len(results), # ← AI can see this
"raw_results": results, # ← AI can't see this, but data is still stored
})Only fields listed in llm_result_fields are sent to the AI. Other fields are stored normally and visible in the panel — they just don't get stuffed into the AI's context (saves tokens).
Other entry options
@plugin_entry supports these additional options:
@plugin_entry(
id="process", # Entry ID (defaults to method name)
name="Process Data", # Display name
description="Process and transform data", # Description (for humans and AI)
timeout=60.0, # Timeout in seconds — auto-cancel if exceeded
kind="service", # Type tag (default "action")
)
async def process(self, data: str):
...Most of the time you only need id, name, description. Use the others as needed. For startup initialization, define a startup lifecycle hook instead of expecting an entry to run automatically.
Dynamic entries: register at runtime
Sometimes you don't know what entries your plugin will have — it might depend on config, user settings, or capabilities returned by an external service. Use dynamic registration:
from plugin.sdk.plugin import lifecycle, Ok
@lifecycle(id="startup")
async def on_startup(self):
# Suppose config defines a set of commands
cfg = await self.config.dump()
commands = cfg.get("commands", {})
for cmd_id, cmd_info in commands.items():
self.register_dynamic_entry(
entry_id=cmd_id,
handler=self._make_handler(cmd_info),
name=cmd_info.get("name", cmd_id),
description=cmd_info.get("description", ""),
input_schema=cmd_info.get("input_schema", {
"type": "object",
"properties": {
"cmd": {"type": "string", "description": "Command input"},
},
}),
)
self.logger.info("Registered {} dynamic entries", len(commands))
return Ok({"status": "ready"})
def _make_handler(self, cmd_info):
template = cmd_info.get("template", "Executed: {cmd}")
async def handler(cmd: str = ""):
return Ok({"output": template.format(cmd=cmd)})
return handlerDynamic entries work just like static ones — visible in the panel, executable, callable by the AI. If the handler accepts parameters, pass input_schema explicitly; dynamic registration does not infer it from the handler signature.
Use self.unregister_dynamic_entry(entry_id) to remove them.
Summary
| Scenario | Approach |
|---|---|
| Simple function, few params | @plugin_entry + type annotations in signature |
| Want param descriptions | Use Annotated[type, "description"] |
| Many params / need validation | Define a Pydantic model as single parameter |
| Return success | return Ok({...}) |
| Return failure | return Err(SdkError("reason")) |
| Limit what AI sees | llm_result_fields=["field1", "field2"] |
| Don't know entries until runtime | self.register_dynamic_entry(...) |
