Troubleshooting

Debugging plugins

Plugins fail silently by design — a misbehaving plugin shouldn't crash the agent loop. The flip side is that "why isn't my plugin doing anything?" is the most common debugging question. This page walks through the diagnostic patterns we've found useful.

The audit log pattern

Every non-trivial plugin should write a JSONL audit log under $FLOWLY_HOME/<plugin-name>/log.jsonl. This is the single most useful debugging tool you have — when a plugin runs in a gateway process, you can't attach a debugger, but you can always tail the log file.

python
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


def _log(record: dict[str, Any]) -> None:
    """Append a JSON line to the audit log. Best-effort, never raises."""
    try:
        log_dir = _get_state_dir()  # $FLOWLY_HOME/<plugin>/
        log_dir.mkdir(parents=True, exist_ok=True)
        record["ts"] = datetime.now(timezone.utc).isoformat()
        with open(log_dir / "log.jsonl", "a") as f:
            f.write(json.dumps(record) + "\n")
    except OSError:
        pass  # never let logging break the agent

Events worth logging

  • hook_fired — every time your hook is invoked, with the tool name and the candidate inputs. Critical for debugging "is the hook even running?".
  • committed / tracked / skipped — successful operations.
  • commit_failed / stage_failed — failure modes, with error string.
  • outside_repo / not_allowed — silent skips that would otherwise be invisible.
Always log, especially silent skips
The biggest debugging mistake plugins make: a filter rejects an input and returns silently. From the user's perspective, the plugin did nothing. Always log why — even at info level — so inspecting the log file reveals which gate the input hit.

Common symptoms

Plugin not loaded

Symptom: Slash command returns "unknown command", tool isn't in flowly plugins list, hook never fires.

Diagnostic steps:

  1. flowly plugins list — does your plugin appear? If status is error, the manifest parsed but register(ctx) raised. Check the gateway log for the traceback.
  2. flowly plugins list showing nothing? The manifest is malformed. Try cat ~/.flowly/plugins/<name>/plugin.yaml and verify YAML is valid.
  3. flowly plugins list shows not in plugins.enabled? Run flowly plugins enable <name>.
  4. Did you restart the gateway after enabling? Plugin discovery only runs at startup. flowly service restart or quit and reopen the desktop app.

Hook fires but nothing happens

Symptom: Plugin is loaded, the agent does something that should trigger your hook, but no commit / no track / no audit event.

Add a fired event at the top of your hook:

python
def on_post_tool_call(ctx):
    _log({
        "event": "hook_fired",
        "tool": ctx.tool_name,
        "params_keys": list((ctx.params or {}).keys()),
        "success": ctx.success,
    })
    # ... rest of your logic

Now tail -f ~/.flowly/<plugin>/log.jsonl and trigger an action. Three possible outcomes:

  • No hook_fired event — your hook isn't actually registered, or the runtime never calls it for this tool/event combination. Check register(ctx) wiring.
  • hook_fired appears but no follow-up — a downstream filter is rejecting the input. Add _log calls at every early-return so you see which gate caught it.
  • hook_fired + error event — an exception inside your handler. The error string in the log tells you what.
Real example from disk-cleanup → auto-commit migration
On macOS, /tmp is a symlink to /private/tmp. The auto-commit plugin's _find_git_root returned an unresolved path while another step called path.resolve(), so relative_to raised ValueError silently. We caught it by adding an outside_repo log event at every early-return point. Lesson: never except: return without logging.

Tool not in agent's tool list

Symptom: You registered my_tool, the plugin shows enabled in flowly plugins list, but when the agent says "I don't have access to that tool" or never calls it.

Check:

  1. Is the tool name unique? If two plugins (or a plugin and a built-in) register the same name, last writer wins. Run flowly plugins list with verbose output if available, or grep gateway logs for registered tool.
  2. Is the JSON Schema valid? An invalid schema may make the tool unusable by the model. Try a minimal schema first:
    python
    schema = {"parameters": {"type": "object", "properties": {}}}
  3. Is check_fn always returning False? It runs at dispatch time. Log inside it to verify.

Slash command says "unknown"

Symptom: Typing /your-cmd in the desktop UI returns "Unknown command".

Likely cause: The desktop UI maintains its own slash whitelist for autocomplete and rejects unknown ones at the input layer. Workaround: prefix with a single quote ('/your-cmd args) — the desktop sees this as a regular message and ships it to the gateway, where the plugin slash dispatcher picks it up.

From other channels (Telegram, Web), this isn't a problem.

Tool runs but action fails silently

Symptom: Hook fires, no error in the log, but the promised action (commit, file write, API call) didn't happen.

Diagnostic:

  • For shell-out plugins (git, ffmpeg, etc.), capture both stdout and stderr and log them on non-zero exit. subprocess.run(...) with capture_output=True.
  • For API-calling plugins, log status code and response body on non-200.
  • File operations: check for path-resolution surprises (symlinks, relative paths, expansion of ~).

Agent has my tool but never calls it

Symptom: Plugin loads, slash commands work, the tool shows up in flowly plugins list, but when the user asks something the tool would handle, the agent calls memory_search or knowledge_graph instead — your tool is just ignored.

Root cause: tool descriptions are sent to the model in the API tools parameter, not in the system prompt. The model picks tools based on the description alone. If your description is generic — "Search local data" — the model can't tell when it applies. Worse, your plugin's domain (e.g. legal cases) is invisible to the model unless something tells it.

Two fixes:

  • Sharper tool descriptions — include trigger keywords and a one-line "use this when…" clause.
    python
    # Generic — model can't tell when to use it
    description="Search the database"
    
    # Specific — model knows the domain and the trigger
    description="Find a legal case by client name. Use this FIRST when "
                "the user mentions a person's name in a legal context "
                "(dava, müvekkil, dosya) — before memory_search."
  • Domain context via pre_llm_call — register a hook that runs before each LLM call and returns a string describing the user's domain. The string is wrapped in <plugin_context> tags and prepended to the user message. The model sees it and reaches for your tools.
    python
    def _inject_context(ctx) -> str | None:
        return (
            "User is a Turkish family-law attorney with active cases. "
            "When they mention a client name (Mehmet, Ayşe, Demir), "
            "use case_search BEFORE memory_search."
        )
    
    def register(ctx):
        ctx.register_hook("pre_llm_call", _inject_context)
Real example
The bundled case-tracker plugin had exactly this problem: 6 case_* tools registered, but the agent kept falling back to memory_search. After adding a 30-line pre_llm_call hook listing the available tools and trigger words, the agent now reliably picks case_search case_get for any client-name query. Same tools, same model, same prompt — just better orientation.

Reading gateway logs

When you start the gateway from a terminal, it streams logs to stdout. Useful events:

text
# Plugin discovery — at startup
loaded plugin <name> (tools=N hooks=N commands=N)
plugin discovery complete: N found, M enabled

# Tool dispatch — every agent tool call
Executing tool: <tool_name>(...)
Tool success: <tool_name> result=...
Tool failed: <tool_name> result=Error...

# Session lifecycle
Processing message from <channel>:<chat_id>

If your plugin's loaded plugin ... line is missing, the plugin failed to load and Flowly logged the traceback above it.

Logger filtering
Some Flowly versions filter the flowly.plugins namespace at INFO level by default. If you don't see your discovery line, this is probably why. Set LOGURU_LEVEL=DEBUG before starting the gateway to bypass.

Hard reset checklist

When everything is broken and you want a clean state:

bash
# 1. Stop the gateway
flowly service stop  # or close the desktop app

# 2. Disable, remove, re-install the plugin
flowly plugins disable <name>
flowly plugins remove <name> --yes
flowly plugins install <source>
flowly plugins enable <name>

# 3. Wipe the plugin's state directory
rm -rf ~/.flowly/<plugin-name>/

# 4. Start the gateway with debug logging
LOGURU_LEVEL=DEBUG flowly gateway

# 5. Trigger the action and tail the log
tail -f ~/.flowly/<plugin-name>/log.jsonl
If you've narrowed the problem down to a specific step (e.g." hook fires, but git commit fails with auth error"), the plugin author can usually help you fast — paste the relevant audit log lines and they'll know exactly which branch of the code to check.