DA

datasette-plugin-writer

Guides the development of Datasette plugins using cookiecutter templates and core plugin hooks.

Install

mkdir -p .claude/skills/datasette-plugin-writer && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/5761" && unzip -o skill.zip -d .claude/skills/datasette-plugin-writer && rm skill.zip

Installs to .claude/skills/datasette-plugin-writer

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Guide for writing Datasette plugins. This skill should be used when users want to create or develop plugins for Datasette, including information about plugin hooks, the cookiecutter template, database APIs, request/response handling, and plugin configuration.
259 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)
Intermediate

Key capabilities

  • Bootstrap new plugin projects using cookiecutter templates
  • Define custom SQLite hooks via prepare_connection
  • Register custom URL routing
  • Configure static asset and template directories

How it works

Utilizes a standard cookiecutter template to generate a structured Python repository configured for Datasette's plugin architecture.

Inputs & outputs

You give it
Plugin metadata and template variables
You get back
Scaffolded python package with defined hooks

When to use datasette-plugin-writer

  • Create a new Datasette plugin
  • Implement plugin hooks for Datasette
  • Handle custom request responses in plugins
  • Configure plugin static assets and templates

About this skill

Writing Datasette Plugins

Use this skill to build plugins for Datasette, the open source multi-tool for exploring and publishing data.

Quick Start with Cookiecutter Template

Start a new plugin using the datasette-plugin cookiecutter template with newline-delimited variables:

echo "plugin_name
description of plugin
plugin-hyphenated-name
plugin_underscored_name
github_username
Author Name
y
y" | uvx cookiecutter gh:simonw/datasette-plugin

Example for a plugin called "my-cool-plugin":

echo "my cool plugin
A plugin that does cool things
my-cool-plugin
my_cool_plugin
username
Your Name
y
y" | uvx cookiecutter gh:simonw/datasette-plugin

The last two y responses enable static/ and templates/ directories.

After creating the plugin:

cd datasette-my-cool-plugin
python -m venv venv
source venv/bin/activate
pip install -e '.[test]'
datasette plugins  # Verify plugin is visible
python -m pytest   # Run tests

Plugin Structure

A typical plugin structure:

datasette-my-plugin/
├── datasette_my_plugin/
│   ├── __init__.py        # Plugin hooks go here
│   ├── static/            # Optional: CSS, JavaScript
│   └── templates/         # Optional: Custom templates
├── tests/
│   └── test_my_plugin.py
├── setup.py or pyproject.toml
└── README.md

Essential Plugin Hooks

prepare_connection(conn, database, datasette)

Register custom SQL functions. Called when SQLite connections are created:

from datasette import hookimpl

@hookimpl
def prepare_connection(conn):
    conn.create_function("hello_world", 0, lambda: "Hello world!")

register_routes(datasette)

Add custom URL routes. Return list of (regex, view_function) pairs:

from datasette import hookimpl, Response

async def my_page(request):
    return Response.html("<h1>Hello!</h1>")

@hookimpl
def register_routes():
    return [
        (r"^/-/my-page$", my_page)
    ]

View functions can accept: datasette, request, scope, send, receive.

render_cell(row, value, column, table, database, datasette, request)

Customize how table cell values are displayed:

from datasette import hookimpl
import markupsafe

@hookimpl
def render_cell(value, column):
    if column == "stars":
        return markupsafe.Markup("⭐" * int(value))

extra_template_vars(template, database, table, columns, view_name, request, datasette)

Add variables to template context:

@hookimpl
def extra_template_vars(request, datasette):
    return {
        "user_agent": request.headers.get("user-agent"),
        "custom_data": "value"
    }

Can also return async functions for database queries.

table_actions(datasette, actor, database, table, request)

Add menu items to table pages:

@hookimpl
def table_actions(datasette, database, table):
    return [{
        "href": datasette.urls.path(f"/-/export/{database}/{table}"),
        "label": "Export this table",
        "description": "Download as CSV"
    }]

actor_from_request(datasette, request)

Implement authentication. Return actor dict or None:

@hookimpl
def actor_from_request(request):
    token = request.args.get("_token")
    if token == "secret":
        return {"id": "user123", "name": "Alice"}

Can return async function for database lookups.

permission_allowed(datasette, actor, action, resource)

Control permissions. Return True (allow), False (deny), or None (no opinion):

@hookimpl
def permission_allowed(actor, action, resource):
    if action == "execute-sql" and actor and actor.get("id") == "admin":
        return True

Request and Response Objects

Request Object

Available in many plugin hooks:

request.method          # "GET" or "POST"
request.url             # Full URL
request.path            # Path without query string
request.full_path       # Path with query string
request.query_string    # Query string without ?
request.args            # MultiParams object for query params
request.args.get("key") # Get single param value
request.args.getlist("key")  # Get list of values
request.headers         # Dict of headers (lowercase keys)
request.cookies         # Dict of cookies
request.actor           # Current authenticated actor or None
request.url_vars        # Variables from URL regex

# Async methods:
body = await request.post_body()      # Raw POST body as bytes
form_vars = await request.post_vars() # Form data as dict

Response Object

Create responses in view functions:

from datasette.utils.asgi import Response

# HTML response
return Response.html("<h1>Hello</h1>")

# JSON response
return Response.json({"status": "ok"})

# Text response
return Response.text("Plain text")

# Redirect
return Response.redirect("/other-page")

# Custom response
return Response(
    body="Content",
    status=200,
    headers={"X-Custom": "value"},
    content_type="text/plain"
)

# Set cookies
response = Response.html("<h1>Hello</h1>")
response.set_cookie("session", datasette.sign({"id": "123"}, "cookie"))

Database API

Access databases in plugins:

# Get database object
db = datasette.get_database("mydb")  # Named database
db = datasette.get_database()        # First database

# Execute read query
results = await db.execute("SELECT * FROM mytable WHERE id = ?", [123])
for row in results:
    print(row["column_name"])

# Query properties
results.rows         # List of Row objects
results.columns      # List of column names
results.truncated    # True if results were truncated
results.first()      # First row or None
results.single_value()  # Single value if query returned one

# Execute write query
await db.execute_write(
    "INSERT INTO mytable (name) VALUES (?)",
    ["value"]
)

# Execute multiple writes
await db.execute_write_many(
    "INSERT INTO mytable (id, name) VALUES (?, ?)",
    [(1, "Alice"), (2, "Bob")]
)

# Execute function with write connection
def insert_and_count(conn):
    conn.execute("INSERT INTO mytable (name) VALUES (?)", ["Alice"])
    return conn.execute("SELECT COUNT(*) FROM mytable").fetchone()[0]

count = await db.execute_write_fn(insert_and_count)

# Introspection
tables = await db.table_names()
views = await db.view_names()
columns = await db.table_columns("mytable")
exists = await db.table_exists("mytable")

Plugin Configuration

Users configure plugins in datasette.yaml:

plugins:
  datasette-my-plugin:
    api_key: secret123
    enabled: true

Or per-database:

databases:
  mydb:
    plugins:
      datasette-my-plugin:
        setting: value

Access in plugin code:

config = datasette.plugin_config("datasette-my-plugin")
api_key = config.get("api_key") if config else None

# With database/table context
config = datasette.plugin_config(
    "datasette-my-plugin",
    database="mydb",
    table="mytable"
)

Configuration lookup: table → database → instance level.

Static Assets and Templates

Static Files

Place in static/ directory, reference with:

# In Python
url = datasette.urls.static_plugins("datasette_my_plugin", "app.js")

# In templates
<script src="{{ urls.static_plugins('datasette_my_plugin', 'app.js') }}"></script>

Templates

Place in templates/ directory. Override Datasette templates:

  • database.html - Database page
  • table.html - Table page
  • row.html - Row page
  • query.html - Query page

Access template functions:

{{ csrftoken() }}                    {# CSRF token for forms #}
{{ urls.instance() }}                {# Homepage URL #}
{{ urls.database("mydb") }}          {# Database URL #}
{{ urls.table("mydb", "mytable") }}  {# Table URL #}

Common Patterns

Add a custom SQL function

@hookimpl
def prepare_connection(conn):
    import hashlib
    def md5(text):
        return hashlib.md5(text.encode()).hexdigest()
    conn.create_function("md5", 1, md5)

Add a custom page

@hookimpl
def register_routes(datasette):
    async def stats_page(request):
        db = datasette.get_database()
        tables = await db.table_names()
        return Response.html(
            await datasette.render_template(
                "stats.html",
                {"tables": tables},
                request=request
            )
        )

    return [(r"^/-/stats$", stats_page)]

Render custom output format

@hookimpl
def register_output_renderer(datasette):
    def render_csv(columns, rows):
        import csv, io
        output = io.StringIO()
        writer = csv.writer(output)
        writer.writerow(columns)
        writer.writerows(rows)
        return Response(
            output.getvalue(),
            content_type="text/csv"
        )

    return {
        "extension": "csv",
        "render": render_csv
    }

Check permissions

@hookimpl
def register_routes(datasette):
    async def admin_page(request):
        # Check if user has permission
        allowed = await datasette.permission_allowed(
            request.actor,
            "admin-page",
            default=False
        )
        if not allowed:
            from datasette import Forbidden
            raise Forbidden("Admin access required")

        return Response.html("<h1>Admin Page</h1>")

    return [(r"^/-/admin$", admin_page)]

URL Design

Use /-/ prefix to avoid conflicts with database names:

  • /-/my-plugin - Instance-level page
  • /dbname/-/my-plugin - Database-level page
  • /dbname/table/-/my-plugin - Table-level page

Build URLs with base_url support:

datasette.urls.path("/-/my-page")
datasette.urls.database("mydb")
datasette.urls.table("mydb", "mytable")

Testing Plugins

Create tests in tests/test_my_plugin.py:

from datasette.app import Datasette
import pytest

@pytest.mark.asyncio
async def test_my_plugin():
    datasette = Datasette()
    await datasette.invoke_startup()

    # Test with client
    r

---

*Content truncated.*

When not to use it

  • Writing logic that should be a simple script instead of a plugin
  • Applications not utilizing the Datasette ecosystem

Prerequisites

uvxcookiecutter

Limitations

  • Requires familiarity with Python packaging
  • Template structure is opinionated

How it compares

It enforces the specific Datasette plugin architecture and hook registration patterns instead of arbitrary code structure.

Compared to similar skills

datasette-plugin-writer side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
datasette-plugin-writer (this skill)17moReviewIntermediate
copilot-sdk74moReviewIntermediate
api-test-generator19moReviewIntermediate
generating-api-sdks127dReviewAdvanced

Try saying

Example prompts that trigger this skill in your AI assistant.

You might also like

copilot-sdk

github

Build agentic applications with GitHub Copilot SDK. Use when embedding AI agents in apps, creating custom tools, implementing streaming responses, managing sessions, connecting to MCP servers, or creating custom agents. Triggers on Copilot SDK, GitHub SDK, agentic app, embed Copilot, programmable agent, MCP server, custom agent.

763

api-test-generator

mikopbx

Генерация полных Python pytest тестов для REST API эндпоинтов с валидацией схемы. Использовать при создании тестов для новых эндпоинтов, добавлении покрытия для CRUD операций или валидации соответствия API с OpenAPI схемами.

16

generating-api-sdks

jeremylongshore

Generate client SDKs in multiple languages from OpenAPI specifications. Use when generating client libraries for API consumption. Trigger with phrases like "generate SDK", "create client library", or "build API SDK".

13

agentica-sdk

parcadei

Build Python agents with Agentica SDK - @agentic decorator, spawn(), persistence, MCP integration

12

generating-rest-apis

jeremylongshore

Generate complete REST API implementations from OpenAPI specifications or database schemas. Use when generating RESTful API implementations. Trigger with phrases like "generate REST API", "create RESTful API", or "build REST endpoints".

02

replit-sdk-patterns

jeremylongshore

Apply production-ready Replit SDK patterns for TypeScript and Python. Use when implementing Replit integrations, refactoring SDK usage, or establishing team coding standards for Replit. Trigger with phrases like "replit SDK patterns", "replit best practices", "replit code patterns", "idiomatic replit".

11

Search skills

Search the agent skills registry