Function Dev logo

Function Dev

Organization
butterbase-ai
function-dev

Use when developing, deploying, or debugging Butterbase serverless functions, or when the user needs to add backend logic like webhooks, scheduled jobs, or custom API endpoints

Overview

Publisherbutterbase-ai
Repositorybutterbase-skills
Skill namefunction-dev
Stars
535
Forks
42
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by butterbase-ai on GitHub. Read the source before you install it.

Installation

Install the Function Dev AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/butterbase-ai/butterbase-skills/tree/main/skills/function-dev
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/butterbase-ai/butterbase-skills.git /tmp/butterbase-skills
mkdir -p .claude/skills
cp -r /tmp/butterbase-skills/skills/function-dev .claude/skills/function-dev
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Function Dev in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Function Dev on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Function Dev is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Serverless Function Development on Butterbase

Guide for developing and deploying serverless functions on Butterbase's Deno runtime. Covers handler signatures, trigger types, database access, environment variables, and testing.


1. Handler Signature

Every function exports a single handler function with this signature:

typescript
export async function handler(
  request: Request,
  context: {
    db: PostgresClient,                                  // RLS-aware DB client
    env: Record<string, string>,                         // env vars set on the function
    user: { id: string } | null,                         // present for HTTP+auth:required; null for cron
    waitUntil: (p: Promise<unknown>) => void,            // background work after Response (≤30s)
    idempotency: {
      claim: (key: string, opts?: { scope?: string; ttlSeconds?: number }) => Promise<boolean>
    }                                                    // atomic dedup for webhook retries
  }
): Promise<Response>

CRITICAL: The handler MUST return new Response() (Web API standard). Do NOT return plain objects.

Correct:

typescript
return new Response(JSON.stringify({ message: "ok" }), {
  status: 200,
  headers: { "Content-Type": "application/json" }
});

Wrong (will fail):

typescript
return { status: 200, body: "ok" };  // NOT a Response object!

2. Trigger Types

HTTP Trigger

Invoke the function via an HTTP request.

json
{
  "trigger": {
    "type": "http",
    "config": { "method": "POST", "path": "/my-endpoint", "auth": "required" }
  }
}

Auth options:

  • "required" — request must include a valid JWT; ctx.user is always set
  • "optional" — JWT is parsed if present; ctx.user may be null
  • "none" — public endpoint; no auth needed; ctx.user is always null

Cron Trigger

Execute the function on a schedule.

json
{
  "trigger": {
    "type": "cron",
    "config": { "schedule": "0 9 * * *", "timezone": "UTC" }
  }
}

Uses standard 5-field cron expressions:

  • "*/5 * * * *" — every 5 minutes
  • "0 0 * * 0" — weekly, Sunday at midnight
  • "0 3 * * *" — daily at 3am UTC
  • "0 9 * * 1-5" — weekdays at 9am

Cron functions run as butterbase_service (RLS bypassed). ctx.user is always null.


WebSocket Trigger

Fire when a connected client sends a matching event over the realtime WebSocket.

json
{
  "trigger": {
    "type": "websocket",
    "config": { "event": "chat-message" }
  }
}

Fires when client sends matching event via realtime WebSocket connection. The request body contains the event payload sent by the client.


S3 Upload Trigger (placeholder — not yet implemented)

json
{
  "trigger": {
    "type": "s3_upload",
    "config": { "prefix": "uploads/", "contentTypes": ["image/*"] }
  }
}

3. Database Access

Use ctx.db.query(sql, params) for all database queries. Always use parameterized queries to prevent SQL injection — NEVER use string interpolation.

typescript
// Always use $1, $2 placeholders — never string interpolation
const { rows } = await ctx.db.query(
  'SELECT * FROM posts WHERE author_id = $1',
  [ctx.user.id]  // params array
);

SELECT

typescript
const { rows } = await ctx.db.query(
  'SELECT * FROM posts WHERE author_id = $1 AND published = true',
  [ctx.user.id]
);

INSERT

typescript
await ctx.db.query(
  'INSERT INTO logs (event, user_id) VALUES ($1, $2)',
  ['page_view', ctx.user.id]
);

UPDATE

typescript
await ctx.db.query(
  'UPDATE posts SET title = $1, updated_at = now() WHERE id = $2 AND author_id = $3',
  [newTitle, postId, ctx.user.id]
);

RLS Behavior by Invocation Type

InvocationRoleRLS
End-user JWTbutterbase_userEnforced — ctx.db queries filtered by policies
API key (bb_sk_)butterbase_serviceBypassed — sees all data
Cron triggerbutterbase_serviceBypassed — sees all data

4. Environment Variables

  • Set at deploy time: pass envVars parameter to deploy_function
  • Update without redeploying: use update_function_env
  • Access in handler: ctx.env.VARIABLE_NAME
  • Encrypted at rest: values are never exposed in logs or API responses

Common uses: API keys, webhook secrets, external service URLs.

typescript
const apiKey = ctx.env.OPENAI_API_KEY;
const webhookSecret = ctx.env.WEBHOOK_SECRET;
const serviceUrl = ctx.env.EXTERNAL_SERVICE_URL;

5. Complete Working Examples

Example 1 — Protected API Endpoint (auth: required)

Returns the authenticated user's posts.

typescript
export async function handler(req, ctx) {
  const { rows } = await ctx.db.query(
    'SELECT id, title, created_at FROM posts WHERE author_id = $1 ORDER BY created_at DESC',
    [ctx.user.id]
  );
  return new Response(JSON.stringify(rows), {
    headers: { "Content-Type": "application/json" }
  });
}

Deploy:

deploy_function(
  app_id,
  name: "my-posts",
  code: ...,
  trigger: {
    type: "http",
    config: { method: "GET", path: "/my-posts", auth: "required" }
  }
)

Example 2 — Webhook Receiver (auth: none)

Accepts an incoming webhook, validates the signature, and stores the event.

typescript
export async function handler(req, ctx) {
  const body = await req.json();
  const signature = req.headers.get("x-webhook-signature");
  // Validate signature against ctx.env.WEBHOOK_SECRET
  await ctx.db.query(
    'INSERT INTO webhook_events (event_type, payload) VALUES ($1, $2)',
    [body.type, JSON.stringify(body)]
  );
  return new Response("ok", { status: 200 });
}

Deploy with: trigger: { type: "http", config: { method: "POST", path: "/webhook", auth: "none" } }


Example 3 — Cron Cleanup Job

Deletes expired sessions on a nightly schedule.

typescript
export async function handler(req, ctx) {
  const result = await ctx.db.query(
    "DELETE FROM sessions WHERE expires_at < now() RETURNING id"
  );
  return new Response(JSON.stringify({ deleted: result.rowCount }), {
    headers: { "Content-Type": "application/json" }
  });
}

Deploy with: trigger: { type: "cron", config: { schedule: "0 3 * * *" } }


Example 4 — External API Call

Proxies a request to an external AI service using a stored API key.

typescript
export async function handler(req, ctx) {
  const { prompt } = await req.json();
  const response = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${ctx.env.OPENAI_API_KEY}`
    },
    body: JSON.stringify({
      model: "gpt-4",
      messages: [{ role: "user", content: prompt }]
    })
  });
  const data = await response.json();
  return new Response(JSON.stringify(data), {
    headers: { "Content-Type": "application/json" }
  });
}

Deploy with envVars: { OPENAI_API_KEY: "sk-..." } and trigger: { type: "http", config: { method: "POST", auth: "required" } }


Example 5 — Error Handling Pattern

Always wrap handler logic in try/catch and return a proper error Response.

typescript
export async function handler(req, ctx) {
  try {
    const { id } = await req.json();
    const { rows } = await ctx.db.query(
      'SELECT * FROM items WHERE id = $1',
      [id]
    );
    if (rows.length === 0) {
      return new Response(JSON.stringify({ error: "Not found" }), {
        status: 404,
        headers: { "Content-Type": "application/json" }
      });
    }
    return new Response(JSON.stringify(rows[0]), {
      status: 200,
      headers: { "Content-Type": "application/json" }
    });
  } catch (err) {
    return new Response(JSON.stringify({ error: "Internal server error" }), {
      status: 500,
      headers: { "Content-Type": "application/json" }
    });
  }
}

6. Testing & Debugging

The standalone tools deploy_function and invoke_function are unchanged. Everything else (logs, env updates, listing, deletion) is handled by manage_function with an action parameter.

Invoke a Function

invoke_function(
  app_id: "app_abc123",
  function_name: "my-function",
  method: "POST",
  body: { key: "value" }
)

Returns the full HTTP response (status, headers, body, duration_ms). Use this immediately after deploying to verify behavior.

View Error Logs

manage_function(
  app_id: "app_abc123",
  action: "get_logs",
  function_name: "my-function",
  level: "error"
)

Returns recent invocations with errors, stack traces, and captured console.log/warn/error output.

View All Logs

manage_function(
  app_id: "app_abc123",
  action: "get_logs",
  function_name: "my-function",
  limit: 100,
  since: "2026-01-15T00:00:00Z"
)

Filters: limit (default 100), since (ISO timestamp), level ("error" or "all").

List Functions & Metrics

manage_function(app_id: "app_abc123", action: "list")

Returns each function's name, trigger, URL, status, and metrics (invocationCount, errorRate, avgDuration, lastInvoked).


7. Common Mistakes

MistakeFix
Returning plain object instead of ResponseAlways use new Response(JSON.stringify(data), { headers: {...} })
SQL injection via string interpolationUse parameterized queries: $1, $2 placeholders
Not wrapping in try/catchAlways catch errors and return a Response with error status
Forgetting async on handlerHandler must be async function handler(...)
Exceeding timeout (30s default)Increase timeoutMs in deploy_function or optimize the function
Not setting Content-Type headerAlways include "Content-Type": "application/json" for JSON responses

8. Quick Reference

Deploy a Function

deploy_function(
  app_id: "app_abc123",
  name: "my-function",
  code: "export async function handler(req, ctx) { ... }",
  trigger: { type: "http", config: { method: "POST", auth: "required" } },
  envVars: { MY_SECRET: "value" },
  timeoutMs: 30000,       // default: 30s, max: 300s
  memoryLimitMb: 128      // default: 128MB
)

Update Env Vars (without redeploying)

manage_function(
  app_id: "app_abc123",
  action: "update_env",
  function_name: "my-function",
  env: { MY_SECRET: "new-value", DELETE_ME: null }   // null deletes the key
)

Delete a Function

manage_function(
  app_id: "app_abc123",
  action: "delete",
  function_name: "my-function"
)

Invocation URL Pattern

https://api.butterbase.ai/v1/{app_id}/fn/{function-name}

For HTTP triggers, this is the URL clients call directly.


If a docs/butterbase/00-state.md exists in the working directory, prefer invoking via /butterbase-skills:journey-functions so the journey orchestrator stays in sync.

Frequently asked questions

What does the Function Dev AI skill do?

Use when developing, deploying, or debugging Butterbase serverless functions, or when the user needs to add backend logic like webhooks, scheduled jobs, or custom API endpoints

Why use Function Dev on TypingMind?

Because you install it once and use it with any model. Function Dev is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Function Dev in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/butterbase-ai/butterbase-skills/tree/main/skills/function-dev. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Function Dev?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Function Dev?

As many as you like. As long as a model supports skills, you can use Function Dev with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Function Dev AI skill free?

Yes. It is published on GitHub by butterbase-ai under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇