API reference

PluginContext API

The ctx object passed to your register(ctx) function is the only public surface plugins use. Four registration methods cover every plugin capability.

register_tool

ctx.register_tool(*, name, schema, handler, check_fn=None, description='')

Register a new tool the agent can call. Handler can be sync or async; Flowly handles both. The schema follows JSON Schema semantics — typically an object with properties and required fields.

ParameterTypeDescription
namestrTool name as the agent sees it. Must be unique across all loaded tools.
schemadictJSON Schema for tool parameters. May be the parameters dict directly or the full OpenAI function schema with parameters inside.
handlerCallableSync or async function. Receives tool arguments as keyword args, returns a string (or any value coerced to string).
check_fnCallable | NoneOptional zero-arg callable returning bool. Runs at dispatch time, not register time. When False, the tool returns "unavailable" without calling handler. Useful for OAuth-gated tools.
descriptionstrHuman-readable description shown to the agent. Falls back to schema.description if empty.
python
def register(ctx):
    async def lookup(query: str, limit: int = 10) -> str:
        results = await my_api.search(query, limit=limit)
        return "\n".join(r.title for r in results)

    ctx.register_tool(
        name="my_search",
        schema={
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string"},
                    "limit": {"type": "integer", "default": 10},
                },
                "required": ["query"],
            },
        },
        handler=lookup,
        check_fn=lambda: bool(os.getenv("MY_API_KEY")),
        description="Search my service",
    )

register_hook

ctx.register_hook(event_name, callback)

Subscribe to a lifecycle event. The callback receives an event-specific context dataclass (e.g. ToolHookContext, SessionHookContext). It can be sync or async.

The callback may return an action object to influence runtime flow:

  • BlockAction(message=...) from pre_tool_call aborts dispatch with that message
  • SkipAction(reason=...) from pre_gateway_dispatch drops the inbound message silently
  • RewriteAction(text=...) from pre_gateway_dispatch replaces the message text
  • A bare str from transform_tool_result / transform_terminal_output replaces the original output

Common patterns:

Block dangerous tool calls — return BlockAction from pre_tool_call:

python
from flowly.agent.hooks import BlockAction


def register(ctx):
    def reject_root_writes(hook_ctx):
        if hook_ctx.tool_name == "write_file":
            path = hook_ctx.params.get("path", "")
            if path.startswith("/etc/"):
                return BlockAction("writes to /etc/ are not allowed")

    ctx.register_hook("pre_tool_call", reject_root_writes)

Inject domain context before LLM calls — return a string from pre_llm_call. The string is wrapped in <plugin_context> tags and prepended to the last user message. Useful when your plugin's tools won't be picked up by the model unless you explicitly orient it toward your domain:

python
def register(ctx):
    def add_legal_context(ctx):
        return (
            "User is a lawyer with active cases. When you see a client "
            "name or 'dava' / 'müvekkil', prefer case_search over "
            "memory_search. Available: case_search, case_get, case_list, "
            "case_create, case_update, case_add_event."
        )

    ctx.register_hook("pre_llm_call", add_legal_context)

Audit trail of every tool call — observe-only with post_tool_call:

python
def register(ctx):
    def log_tool(ctx):
        _log({"tool": ctx.tool_name, "duration_ms": ctx.duration_ms,
              "success": ctx.success})

    ctx.register_hook("post_tool_call", log_tool)
See the full event catalogue on Hook Events.

register_command

ctx.register_command(name, handler, description='', args_hint='')

Register an in-session slash command. Available across all channels — Telegram, Web, Desktop, iOS — wherever the user can type into a chat.

ParameterTypeDescription
namestrCommand name without the leading slash. Lowercased and hyphenated automatically (/Foo Bar → /foo-bar).
handlerCallable[[str], str | None]Sync or async. Receives raw_args (everything after the command name). Returns a string to send back, or None for fire-and-forget.
descriptionstrShown in /help and the desktop UI command list.
args_hintstrOptional hint like "<city>" or "id:42 mode:quick" — surfaced by gateways with native command pickers (Discord).
python
def register(ctx):
    def ping(args: str) -> str:
        return "pong" + (f" {args}" if args else "")

    ctx.register_command(
        "ping",
        handler=ping,
        description="Round-trip latency check",
        args_hint="[message]",
    )
Reserved names (/new, /clear, /compact, /help) are rejected with a warning — they belong to Flowly core.

register_skill

ctx.register_skill(name, path, description='')

Register a markdown skill the agent can load explicitly via skill_view("<plugin>:<name>"). Plugin skills don't appear in the system prompt's available-skills index — they are opt-in only — which keeps the prompt cache prefix stable across plugin sets.

ParameterTypeDescription
namestrBare name (no colons). Becomes "<plugin_name>:<name>" when loaded.
pathPathPath to a SKILL.md file. Must exist when register() is called.
descriptionstrDescription shown in flowly skills list.
python
from pathlib import Path


def register(ctx):
    ctx.register_skill(
        name="onboard",
        path=Path(__file__).parent / "skills" / "onboard" / "SKILL.md",
        description="First-run user onboarding flow",
    )

# The agent loads this with:
#     skill_view(name="my-plugin:onboard")